关于这些建议
这些建议并不适用于所有的项目
过程方面
1.避免分支资产 对于任何资产我们应该只有一个版本。如果你非要把一个预设,场景,或网格分支开来,那么情遵循一个过程,这个过程必须清楚的表明哪一个才是正确的版本。错误的分支应该有一个显著的名字,例如,使用双下划线前缀__MainScene_Backup。预设的分支需要一个特定的过程来使其安全。(详见预设部分)
2.持有项目副本 任何一个使用版本控制的团队成员都应该持有项目的副本,用于测试检查。在变动之后,副本,这一干净的版本,应该更新并测试。任何人都不能对干净的副本做任何改变。当找回丢失的东西的时候,这一点就会发挥作用。
3.考虑使用外部关卡工具来编辑关卡 Unity并不是完美的关卡编辑器。例如,我们曾经使用TuDee来为3D游戏建立关卡,这是个能让我们轻松建立砖块的工具(捕捉网格、多倍数的90度旋转、2D视图、快速选择砖块)。从XML文件转为预设的实例是简单的。你可以从Guerrilla Tool Development获得更多的启发。
4.在XML中保存关卡而不是在场景中 这是一个极好的技术
当然你也可以使用Unity作为关卡编辑器(虽然你不需要),你需要写一些代码来序列化和反序列化你的数据,在编辑器内和运行的时候装载关卡,然后在编辑器里保存关卡。你可能还需要模仿Unity的ID系统来维护对象之间的参照。
5.编写自定义的检查代码 写自定义检查相当简单,但是Unity的系统有很多弊端
你可以彻底的重新执行检查系统来解决这些问题。用一些映像技巧,这并不想看起来那么难,具体方法将在文章末尾提供。
6.使用命名为空的游戏对象作为场景文件夹 仔细安排你的场景使它容易找到对象。、
7.在000条件下维护预设和文件夹(空的游戏对象) 如果一个转变专门用于定位一个对象,那么它应该在原点。这样,在本地和世界空间运行出错的风险就更小,代码也会更简单。
8.减少GUI组件的偏移 偏移量应该始终用在父组件的布局组件里;它们不应该依靠更上一级的组件定位。偏移量不应通过互相抵消来正确显示,这基本上是为了防止以下事件:
父容器定位在(100, -50),子容器,应该定位在(10, 10),然后定位在(90, 60)[相对于父容器]。
这种错误在容器隐藏,或者是根本没有可视化表现的情况下是常见。
9.把你的世界基准定义在y = 0。这样更容易把对象放在地面上,在游戏逻辑、AI以及物理方面把世界看成一个2D空间(适当时)。
10.让游戏的每个场景运行流畅。这大大减少了测试时间,要让所有的场景运行流畅,你需要做两件事:
首先,你要找到一种方法仿制所有的数据,这些数据在之前下载的场景里是需要但却是不可用的。
第二,产生的对象必须坚持场景负载之间的下列语句
1
2
3
4
5
6
7
8
9
|
myObject = FindMyObjectInScene();
if
(myObjet ==
null
)
{
myObject = SpawnMyObject();
}
|
美术方面
11.把人物和站立物体的支点放在底部,而不是中心。这样易于将人物和物体精准的放在地面上。同时在游戏逻辑、AI以及物理方面,这也能让3D工程像2D一样简单,当然是在某些合适的情况下。
12. 让所有的网格面向同一方向(正或负Z轴)。这适用于那些有朝向概念的人物或者事物的网格。如果所有的东西都面朝一个方向,那么许多算法都可以得到简化。
13.从一开始就确定尺寸。
14.制作二聚平面用以GUI组件和手动创建粒子。
15.制作和使用的测试技术
l 网格
l 各种纯的颜色:白色,黑色着色试验,50%的灰,红,绿,蓝,黄,洋红,青蓝。
l 阴影检测梯度:黑色到白色,红色,绿色,红色,蓝色,绿色,蓝色。
l 黑白棋盘
l 平滑和崎岖的法线贴图
l 照明设备(如预制)快速建立测试场景
16.对于一切都可以使用预制。游戏场景中的唯一对象不应该是预制,而应该是文件夹。即使是只使用一次的特殊对象也应该是预制。这使得不改变场景也可以轻松实现转变。(这也让使用EZGUI构建sprite地图时更加可靠)
17. 使用不同的预制来专业化,不使用专门的实例。如果你有两个类型的敌人,他们的唯一区别是他们的财产不同,那么对财产分别作预制,然后再将其链接,这让下面两点成为可能:
l 在一个地方对任何类型做改变
l 在不改变场景的情况下做出变化
如果你有太多的敌人类型,那么就不用在编辑器重做出专业化实例了。一种代替方法是做程序,或者使用对所有的敌人使用一个核心文件/预制。一个下降动作可以用于不同的敌人,一个运算可以基于敌人位置或玩家进程。
18.将预制之间链接,实例之间不链接。当把预制拖放到场景时,预制的链接可以得到保证,而实例则不可以。链接到预制可以在任何时候减少场景的建立,也可以减少场景变化的需求。
19.尽可能在实例之间自动建立连接。如果你需要链接实例,建立编程链接。例如,玩家预制可以在GameManager启动时自己注册,或者GameManager在启动时可以找到玩家预制实例。
l 如果你想添加其他脚本的话,就不要把网格放在预制的根源。
l 用链接预制代替嵌套预制。
20.使用安全的流程来分支预制。我们以玩家预制为例来解释:
如下是一个有风险的改变玩家预制的方法:
1. 复制玩家预制
2. 重命名该副本 __Player_Backup
3. 改变玩家预制
4. 如果一切顺利,删除 __Player_Backup
不要把把副本命名为Player_New,并且改变它!
有些情况更加复杂。例如,某个改变可能涉及两个人,按照上述步骤直到Person 2完成的时候,可能会破坏掉所有人的工作场景。如果足够快的话,仍会是这样。因为变化需要的时间更长,你可以仿照下面的方法:
Person 1
1. 复制玩家预制
2. 重命名为__Player_WithNewFeature或者__Player_ForPerson2.
3. 在副本上修改,并且提交到Person 2
Person 2:
1. 在新预制上做修改
2. 复制玩家预制,命名为 __Player_Backup.
3. 拖动__Player_WithNewFeature的实例到场景
4. 拖动这个实例到原来的玩家预制
5. 如果一切顺利,删除__Player_Backup 和 __Player_WithNewFeature.
拓展组件和 MonoBehaviourBase
21.拓展你自己的基本单一行为,并推导出所有它的组件。这让你实现一些通用功能,如类的安全调用和其他更复杂的调用。
22.定义调用的安全方法,StartCoroutine和实例化。定义一个委托任务,并用它来定义不依赖于字符串名称的方法,例如:
1
2
3
4
5
6
7
|
public void Invoke(Task task, float time)
{
Invoke(task.Method.Name, time);
}
|
23.使用共享界面的拓展组件。有时候可以很方便的得到执行某个界面的组件,或者使用组件找到对象。下面的执行使用typeof来代替这些功能的通用版本。通用版本不能使用这些接口,但是typeof可以。请参考:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
//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.使用拓展组件使语法更加方便,例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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)。例如:这使得在检查器中难以改变组件(即使它们是相同的基本类型)。作为替代品,当一个组件需要输出一条没有被发现的错误信息时,下面的GameObject拓展就可以使用了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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.避免使用不同的语法来做同一件事。许多情况下,可以有多种语法来做一件事。这时,请选择一种贯穿项目的始终,因为:
有些语法不能很好地协同工作。只使用一种语法使得设计能够朝着一个方向进行,并且不适合其他语法。
从始至终使用一种语法能让团队成员更好地了解项目进程,可以让架构和代码更容易理解,更少出错。
例子:
·协同VS.状态机。
·嵌套问题VS.相关问题VS.预制
·数据分离策略
·2D游戏状态中使用sprites的方法
·预制结构
·生产策略。
·查找对象的方法:按类型VS.名称VS.标记VS.层VS.参考(“链接”)。
·组对象的方法:类型VS.名称VS.标签VS.层VS.数组引用(“链接”)。
·寻找对象VS.自注册
·控制执行规则(使用Unity的执行规则设置VS.产生逻辑VS.清醒/启动和更新/晚更新依赖VS.手工方法VS.任何规则结构)
·选择对象/位置/用鼠标选择目标:选择管理VS.自我管理
·保持变化场景之间的数据:通过PlayerPrefs,或者加载一个新场景时不会被破坏的物体
·结合方式(混合,添加和分层)动画
产生对象
28.游戏运行时,不要让产生对象弄乱你的层次。当游戏运行时,在场景对象中设置他们的父对象将使东西更容易找到。你可以使用一个空的游戏对象,甚至是单例来使访问代码更容易。将这个对象成为DynamicObjects。
数据结构设计
29.为方便起见请使用单例下述可以使任何数据自动继承单例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public class Singleton<T> : MonoBehaviourwhere T : MonoBehaviour
{
protectedstatic T instance;
/**
Returnsthe instance of this singleton.
*/
publicstatic 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;
}
}
}
|
单例对管理很有用,比如粒子管理、音频管理、GUI管理
30.对于组件,绝不要公开那些不应在检查面板中调整的变量。否则,它建个可能被设计师改变,尤其是在不清楚它的用处的时候。在某些罕见的情况下,这是不可避免的。在这种情况下,请使用双下划线甚至四个下划线来作为变量的名称前缀,以警告那些想要做修改的人。
public float __aVariable;
31.把界面从游戏逻辑中分离出来
32.分离状态和簿记簿记变量是为了高效、方便,并可从状态中恢复。通过分离这些,你可以更容易的:
·保存游戏状态
·调试游戏状态
一种方法是:为每个游戏逻辑类定义一个保存数据类
1
2
3
4
5
6
7
8
9
10
11
|
[Serializable]
PlayerSaveData
{
public float health;
//public forserialisation, not exposed in inspector
}
Player
{
//... bookkeeping variables
//Don’t expose state in inspector. State isnot tweakable.
private PlayerSaveData playerSaveData;
}
|
33.独立专业化设置。考虑两个有着相同网格,但Tweakables不同(例如不同强度和不同速度)的敌人,有不同的方式来分离数据。我倾向于下述方式,特别是当对象被催生或者游戏保存的时候。(Tweakables不是状态数据,而是配置数据,所以不需要保存,当加载或催生对象的时候,Tweakables会自动分别加载)
·定义每个游戏逻辑类的模板类。例如,对敌人,我们还定义了Enemytemplate。所有的分化Tweakables都存储在Enemytemplate
·在游戏逻辑类里,定义一个变量的模板类型。
·做一个敌人的预制件,和两个模板预制weakenemytemplate和strongenemytemplate。
·加载或催生对象时,设置合适模板的模板变量。
这种方法可以变得相当复杂的(有时是不必要的,复杂的,所以要小心!)。
例如,为了更好地利用通用多态性,我们可以这样定义类:
1
2
3
4
5
6
7
8
9
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.避免使用公共指数耦合阵列。例如,不要定义武器阵列,子弹阵列,和颗粒阵列,你的代码看起来像这样:
1
2
3
4
5
6
7
8
9
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
7
|
[Serializable]
public class Weapon
{
publicGameObject prefab;
publicParticleSystem particles;
publicBullet bullet;
}
|
这段代码看上去更整洁,最重要的是,在检查面板里建立数据时不容易出错。
36.避免使用序列以外的数组结构。例如,一个玩家可能有三种攻击类型,每个都使用当前的武器,但是产生不同的子弹和行为。你可能想把三个子弹放在一个数组中,用这种逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public void FireAttack()
{
///behaviour
Fire(bullets[0]);
}
public void IceAttack()
{
///behaviour
Fire(bullets[1]);
}
public void WindAttack()
{
///behaviour
Fire(bullets[2]);
}
枚举可以让代码看起来更好…
public void WindAttack()
{
///behaviour
Fire(bullets[WeaponType.Wind]);
}
|
但是不是在检查面板中。
最好使用独立的变量,名称可以有助于显示应该放入哪些内容。使用一个类来让它整洁。
1
2
3
4
5
6
7
|
[Serializable]
public class Bullets
{
publicBullet FireBullet;
public Bullet IceBullet;
public Bullet WindBullet;
}
|
PS:假设没有其他的火、冰、风等数据。
37.在序列化类中将数据分组以使事物在检查面板中看起来更整洁。一些实体可能有几十个tweakables,这就使得在检查面板中寻找正确的变量成为一场噩梦。以下步骤会让事情变简单:
·对变量组定义独立的类,使其公开并序列化。
·在主类里,为如上定义的每一类型定义公共变量
·不要在Awake 或 Start初始化这些变量,因为他们是序列化的,Unity会处理那些。
·你可以像以前一样通过定义赋值指定缺省值
·这将在检查面板中将变量按可折叠单位分组,便于管理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
[Serializable]
public class MovementProperties
//Not aMonoBehaviour!
{
public float movementSpeed;
public float turnSpeed = 1;
//default provided
}
public class HealthProperties
//Not aMonoBehaviour!
{
public float maxHealth;
public float regenerationRate;
}
public class Player : MonoBehaviour
{
public MovementProperties movementProeprties;
public HealthPorperties healthProeprties;
}
|
文本
38.如果你的故事文本很多,那么请将文本放在一个文件中。不要放在检视面板的编辑器里。要让它在不打开Unity的情况下就可以编辑,特别是在不用保存场景的情况下。
39.如果你打算本地化,把所有字符串独立到一个地方。方法很多,其一是定义每个字符串都有公共字符串字段的文本类,默认设置为英语。例如:其他语言子属于它,然后用语言赋值重新初始化字段。
更先进的技术将读取电子数据表,然后基于所选语言为选择正确的字符串提供逻辑。
测试和调试
40.执行图形记录器调试物理,动画,和AI,这可以使调试相当快。
41.执行HTML记录器。在某些情况下,记录仍然是有用的。记录可以更容易地解析(彩色编码,多个视图,记录截图),可以使日志调试更愉快。
42.执行你自己的FPS计数器。是的,没人知道Unity的FPS计数器到底测量什么,但不是帧速率。执行你自己的,这样数量就可以协调直觉和视觉检查了。
43.快捷键实现屏幕截图。许多bugs是可见的,并且通过图片可以更容易发现。
44.实施快捷方式打印玩家的世界位置。这易于发现bug的位置。
45.执行debug选项使测试更容易,例如
·解锁所有项目
·禁用敌人
·禁用GUI
·让玩家无敌
·禁用所有的游戏
46.对于足够小的团队,为每一个成员做一个有debug选项的预制。将用户标示符放在一个不被承认的文件里,并且在游戏运行时读取,原因是:
·团队成员偶尔会不承认他们的debug选项,还可能影响到别人。
·改变debug选项不改变场景
47.维持所有游戏元素的场景。例如:一个你可以与所有敌人,所有对象互动的场景等等。这可以很容易地测试功能,不用耗费太多精力。
48.为调试快捷键定义常量并保持固定的位置。
49.记录你的设置。绝大多数的文件都应该编码,但是某些东西应该记录在代码之外。让设计师通过设置找代码是在浪费时间。记录设置能提高效率(如果是最近的记录)。
按下列方式记录:
·层使用(碰撞,裁剪,和光线投射–本质,哪一层里应该有什么)
·标签使用
·GUI深度层(应该显示什么)
·场景设置
·语法偏好
·预制结构
·动画层
·命名标准和文件夹结构
50. 遵循文件的命名规则和文件夹结构。统一的命名和文件夹结构更利于查找和辨认。相信你也希望创建自己的命名规则和文件夹结构。这里提供一个例子供参考。
命名的一般原则:
1.是什么就叫什么。一只鸟就应该就应该叫做“Bird”。
2.选择容易发音和记住的名字。如果你在做玛雅语的游戏,不要命名QuetzalcoatisReturn。
3.保持一致。选了一个名字就坚持到底。
4.使用Pascal案例,就像这样: ComplicatedVerySpecificObject。不要使用空格、下划线或者连字符,但是也有一个例外(请参阅命名同一事物的不同方面)。
5.不要用版本号或者表示进程的词汇(WIP,final)。
6.不要用缩写:DVamp@W 应该是 DarkVampire@Walk。
7.在设计文件中使用术语。如果文件中把die animation称作Die,那么请用DarkVampire@Die,而不是DarkVampire@Death.
8.把特定描述放在左边。是DarkVampire而不是VampireDark;是PauseButton而不是 ButtonPaused。
9.有些名字会形成序列。在这些名字中使用数字。例如:PathNode0, PathNode1。要从0开始,而不是从1开始。
10.不会形成序列的名字就不要使用数字。比如Bird0, Bird1, Bird2应该被叫做Flamingo, Eagle, Swallow.
11.给临时对象命名请使用双下划线前缀__Player_Backup.
命名同一事物的不同方面
在核心名称与描述性事物之间使用下划线,例如
·GUI按钮状 EnterButton_Active, EnterButton_Inactive
·纹理 DarkVampire_Diffuse, DarkVampire_Normalmap
·天空盒 JungleSky_Top, JungleSky_North
·LOD组 DarkVampire_LOD0, DarkVampire_LOD1
不要使用本规则来区分不同类型的项目,例如rock_small,rock_large应该是smallrock,largerock。
结构
场景、工程文件夹以及脚本文件夹的组织应当遵循类似的模式。
文件夹结构
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
...
场景结构
Cameras
Dynamic Objects
Gameplay
Actors
Items
...
GUI
HUD
PauseMenu
...
Management
Lights
World
Ground
Props
Structure
...
脚本文件夹结构
ThirdParty
...
MyGenericScripts
Debug
Extensions
Framework
Graphics
IO
Math
...
MyGameScripts
Debug
Gameplay
Actors
Items
...
Framework
Graphics
GUI
...
怎样重新执行Inspector Drawing
1.为所有的编辑定义基类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
BaseEditor<T>: Editor
where T :MonoBehaviour
{
override public void OnInspectorGUI()
{
T data = (T) target;
GUIContent label =
new
GUIContent();
label.text =
"Properties"
;
//
DrawDefaultInspectors(label, data);
if
(GUI.changed)
{
EditorUtility.SetDirty(target);
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
public staticvoid DrawDefaultInspectors<T>(GUIContent label, T target)
where T :
new
()
{
EditorGUILayout.Separator();
Type type =
typeof
(T);
FieldInfo[] fields = type.GetFields();
EditorGUI.indentLevel++;
foreach(FieldInfo field
in
fields)
{
if
(field.IsPublic)
{
if
(field.FieldType ==
typeof
(int))
{
field.SetValue(target,EditorGUILayout.IntField(
MakeLabel(field), (int)field.GetValue(target)));
}
else
if
(field.FieldType ==
typeof
(float))
{
field.SetValue(target,EditorGUILayout.FloatField(
MakeLabel(field), (float)field.GetValue(target)));
}
///etc. for other primitive types
else
if
(field.FieldType.IsClass)
{
Type[] parmTypes =
new
Type[]{field.FieldType};
string methodName =
"DrawDefaultInspectors"
;
MethodInfo drawMethod =
typeof
(CSEditorGUILayout).GetMethod(methodName);
if
(drawMethod ==
null
)
{
Debug.LogError(
"No methodfound: "
+ methodName);
}
bool foldOut =
|