使用Unity3D的50个技巧:Unity3D最佳实践!
关于这些技巧
- 这些是基于我的一些项目经验,项目团队的规模从3人到20人不等;
- 框架结构的可重用性、清晰程度是有代价的——团队的规模和项目的规模决定你要在这个上面付出多少;
- 很多技巧是品味的问题(这里所列的所有技巧,可能有同样好的技术替代方案);
- 一些技巧可能是对传统的Unity开发的一个冲击。例如,使用prefab替代对象实例并不是一个传统的Unity风格,并且这样做的代价还挺高的(需要很多的preffab)。也许这些看起来有些疯狂,但是在我看来是值得的。
流程
1、避免Assets分支
3、考虑使用外部的关卡编辑工具
4、考虑把关卡保存为XML,而非scene
- 它可以让你不必每个场景都设置一遍;
- 他可以加载的更快(如果大多数对象都是在场景之间共享的)。
- 它让场景的版本合并变的简单(就算是Unity的新的文本格式的Scene,也由于数据太多,而让版本合并变的不切实际)。
- 它可以使得在关卡之间保持数据更简便。
5、考虑编写通用的自定义Inspector代码
- 它不支持从继承中获益;
- 它不允许定义字段级别的Inspector组件,而只能是class类型级别。举个例子,如果没有游戏对象都有一个ScomeCoolType字段,而你想在Inspector中使用不同的渲染,那么你必须为你的所有class写Inspector代码。
场景组织6、使用命名的空Game Object来做场景目录
7、把控制对象和场景目录(空Game Objec)放在原点(0,0,0)
8、尽量减少使用GUI组件的offset
9、把世界的地面放在Y=0
10、使游戏可以从每个Scene启动
1
2
3
4
5
6
|
myObject = FindMyObjectInScene ( ) ; if ( myObjet = = null ) { myObject = SpawnMyObject ( ) ; } |
美术11、把角色和地面物体的中心点(Pivot)放在底部,不要放在中间
12、统一所有的模型的面朝向(Z轴正向或者反向)
13、在开始就把Scale搞正确
14、为GUI组件或者手动创建的粒子制作一个两个面的平面模型
15、制作并使用测试资源
- 为SkyBox创建带文字的方形贴图;
- 一个网格(Grid);
- 为Shader测试使用各种颜色的平面:白色,黑色,50%灰度,红,绿,蓝,紫,黄,青;
- 为Shader测试使用渐进色:黑到白,红到绿,红到蓝,绿到蓝;
- 黑白格子;
- 平滑的或者粗糙的法线贴图;
- 一套用来快速搭建场景的灯光(使用Prefa);
Prefabs16、所有东西都使用Prefab
17、对于特例使用单独的Prefab,而不要使用特殊的实例对象
- 在同一个地方修改所有类型
- 在不动用场景的情况下进行修改
如果你有很多敌人的类型,那么也不要在编辑器中使用特殊的实例。一种可选的方案是程序化处理它们,或者为所有敌人使用一个核心的文件/Prefab。使用一个下拉列表来创建不同的敌人,或者根据敌人的位置、玩家的进度来计算。
18、在Prefab之间链接,而不要链接实例对象
19、如果可能,自动在实例对象之间产生链接关系
20、使用安全的流程来处理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;
扩展和MonoBehaviourBase21、扩展一个自己的Mono Behaviour基类,然后自己的所有组件都从它派生
22、为Invoke, StartCoroutine and Instantiate 定义安全调用方法
1
2
3
4
|
public void Invoke ( Task task , float time ) { Invoke ( task.Method.Name , time ) ; } |
23、为共享接口的组件扩展
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
/ / 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、使用扩展来让代码书写更便捷
01
02
03
04
05
06
07
08
09
10
11
|
public static class CSTransform { public static void SetX ( this Transform transform , float x ) { Vector 3 newPosition = new Vector 3 ( x , transform. position .y , transform. position .z ) ; transform. position = newPosition; } ... } |
25、使用防御性的GetComponent()
01
02
03
04
05
06
07
08
09
10
11
12
|
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类,可以使游戏暂停更容易实现
生成对象28、不要让游戏运行时生成的对象搞乱场景层次结构
类设计29、使用单件(Singleton)模式
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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; } } } |
- 对于那些非唯一的prefab实例使用单件管理器(例如Player)。不要为了坚持这条原则把类的层次关系复杂化,宁愿在你的GameManager(或其他合适的管理器中)中持有一个它们的引用。
- 对于外部经常使用的共有变量和方法定义为static,这样你可以这样简便的书写“GameManager.Player”,而不用写成“GameManager.Instance.player”。
30、在组件中不要使用public成员变量,除非它需要在inspector中调节
1
|
public float __aVariable; |
31、把界面和游戏逻辑分开
32、分离状态控制和簿记变量
- 保存游戏状态
- 调试游戏状态
实现方法之一是为每个游戏逻辑定义一个”SaveData“类,例如:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
[Serializable] PlayerSaveData { public float health; / / public for serialisation , not exposed in inspector } Player { / / ... bookkeeping variables / / Don’t expose state in inspector. State is not tweakable. private PlayerSaveData playerSaveData; } |
33、分离特殊的配置
- 为每一个游戏逻辑类定义一个模板 类 。例如,对于敌人,我们来一个“EnemyTemplate”,所有的属性设置变量都保存在这个类中。
- 在游戏逻辑的类中,定义一个上述模板类型的变量。
- 制作一个敌人的Prefab,以及两个模板的Prefab:“WeakEnemyTemplate”和"StrongEnemyTemplate"。
- 在加载或者生成对象是,把模板变量正确的复制。
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
public class BaseTemplate { ... } public class ActorTemplate : BaseTemplate { ... } public class Entity < EntityTemplateType > where EntityTemplateType : BaseTemplate { EntityTemplateType template; ... } public class Actor : Entity < ActorTemplate > { ... } |
34、除了显示用的文本,不要使用字符串
35、避免使用public的数组
01
02
03
04
05
06
07
08
09
10
11
|
public void SelectWeapon ( int index ) { currentWeaponIndex = index ; Player.SwitchWeapon ( weapons[currentWeapon] ) ; } public void Shoot ( ) { Fire ( bullets[currentWeapon] ) ; FireParticles ( particles[currentWeapon] ) ; } |
1
2
3
4
5
6
|
public class Weapon { public GameObject prefab; public ParticleSystem particles; public Bullet bullet; } |
这样代码看起来很整洁,但是更重要的是,在Inspector中设置时就不容易犯错了。
36、在结构中避免使用数组
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
public void FireAttack ( ) { / / / behaviour Fire ( bullets[ 0 ] ) ; } public void IceAttack ( ) { / / / behaviour Fire ( bullets[ 1 ] ) ; } public void WindAttack ( ) { / / / behaviour Fire ( bullets[ 2 ] ) ; } |
使用枚举值可以让代码看起来更好一点:
1
2
3
4
5
|
public void WindAttack ( ) { / / / behaviour Fire ( bullets[WeaponType.Wind] ) ; } |
1
2
3
4
5
6
|
public class Bullets { public Bullet FireBullet; public Bullet IceBullet; public Bullet WindBullet; } |
这里假设没有其他的Fire、Ice、Wind的数据。
37、把数据组织到可序列化的类中,可以让inspector更整洁
- 把这些变量分组定义到不同的类中,并让它们声明为public和serializable;
- 在一个主要的类中,把上述类的实例定义为public成员变量;
- 不用在Awake或者Start中初始化这些变量,因为Unity会处理好它们;
- 你可以定义它们的默认值;
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
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 movementProeprties; public HealthPorperties healthProeprties; } |
文本38、如果你有很多的剧情文本,那么把他们放到一个文件里面。
39、如果你计划实现本地化,那么把你的字符串分离到一个统一的位置。
测试与调试40、实现一个图形化的Log用来调试物理、动画和AI。
41、实现一个HTML的Log。
42、实现一个你自己的帧速率计算器。
43、实现一个截屏的快捷键。
44、实现一个打印玩家坐标的快捷键。
45、实现一些Debug选项,用来方便测试。
- 解锁所有道具;
- 关闭所有敌人;
- 关闭GUI;
- 让玩家无敌;
- 关闭所有游戏逻辑;
46、为每一个足够小的团队,创建一个适合他们的Debug选项的Prefab。
- 团队的成员不会因为意外的提交了自己的Debug设置而影响到其他人。
- 修改Debug设置不需要修改场景。
47、维护一个包含所有游戏元素的场景。
48、定义一些Debug快捷键常量,并把他们保存在统一的地方。
文档
- Layer的使用(碰撞、检测、射线检测——本质上说,什么东西应该在哪个Layer里);
- Tag的使用;
- GUI的depth层级(说什么应该显示在什么之上);
- 惯用的处理方式;
- Prefab结构;
- 动画Layer。
命名规则和目录结构50、遵从一个命名规范和目录结构,并建立文档
- 名字应该代表它是什么,例如鸟就应该叫做 Bird 。
- 选择可以发音、方便记忆的名字。如果你在制作一个玛雅文化相关的游戏,不要把关卡命名为 QuetzalcoatisReturn 。
- 保持唯一性。如果你选择了一个名字,就坚持用它。
- 使用Pascal风格的大小写,例如 ComplicatedVerySpecificObject 。
不要使用空格,下划线,或者连字符,除了一个例外(详见 为同一事物的不同方面命名 一节)。 - 不要使用版本数字,或者标示他们进度的名词( WIP、final )。
- 不要使用缩写: DVamp@W 应该写成 DarkVampire@Walk 。
- 使用设计文档中的术语:如果文档中称呼一个动画为Die,那么使用DarkVampire@Die ,而不要用 DarkVampire@Death 。
- 保持细节修饰词在左侧: DarkVampire ,而不是 VampireDark ;PauseButton ,而不是 ButtonPaused 。举例说明,在Inspector中查找PauseButton,比所有按钮都以Button开头方便。(很多人倾向于相反的次序,认为那样名字可以自然的分组。然而,名字不是用来分组的,目录才是。名字是用来在同一类对象中可以快速辨识的。)
- 为一个序列使用同一个名字,并在这些名字中使用数字。例如 PathNode0, PathNode1 。永远从0开始,而不是1。
- 对于不是序列的情况,不要使用数字。例如 Bird0, Bird1, Bird2 ,本应该是Flamingo, Eagle, Swallow 。
- 为临时对象添加双下划线前缀,例如 __Player_Backup 。
- GUI中的按钮状态:EnterButton_Active、EnterButton_Inactive
- 贴图: DarkVampire_Diffuse, DarkVampire_Normalmap
- 天空盒:JungleSky_Top, JungleSky_North
- LOD分组:DarkVampire_LOD0, DarkVampire_LOD1
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
Materials GUI Effects Meshes Actors DarkVampire LightVampire ... Structures Buildings ... Props Plants ... ... Plugins Prefabs Actors Items ... Resources Actors Items ... Scenes GUI Levels TestScenes Scripts Textures GUI Effects ... |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
Cameras Dynamic Objects Gameplay Actors Items ... GUI HUD PauseMenu ... Management Lights World Ground Props Structure ... |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
ThirdParty ... MyGenericScripts Debug Extensions Framework Graphics IO Math ... MyGameScripts Debug Gameplay Actors Items ... Framework Graphics GUI ... |