使用Unity的50个建议:Part2(译文)

来源:互联网 发布:济南java培训 编辑:程序博客网 时间:2024/06/05 06:18
  • 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.");
                }
             }
        
            returninstance;
          }
       }
    }
    单例对管理很有用,比如粒子管理、音频管理、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
    [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;
    }
    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
    [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;
    }
    2.使用反射和递归来绘制组件
    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 fieldin fields)
       {
          if(field.IsPublic)
          {
             if(field.FieldType ==typeof(int))
             {
                field.SetValue(target,EditorGUILayout.IntField(
                MakeLabel(field), (int)field.GetValue(target)));
             }  
             elseif(field.FieldType ==typeof(float))
             {
                field.SetValue(target,EditorGUILayout.FloatField(
                MakeLabel(field), (float)field.GetValue(target)));
             }
        
             ///etc. for other primitive types
        
             elseif(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 =true;
        
               drawMethod.MakeGenericMethod(parmTypes).Invoke(null,
                   newobject[]
                   {
                      MakeLabel(field),
                      field.GetValue(target)
                   });
             }     
             else
             {
                Debug.LogError(
                   "DrawDefaultInspectors doesnot support fields of type "+
                   field.FieldType);
             }
          }        
       }
        
       EditorGUI.indentLevel--;
    }
    上述方法使用下述帮助
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    private staticGUIContent MakeLabel(FieldInfo field)
    {
       GUIContent guiContent = newGUIContent();     
       guiContent.text =field.Name.SplitCamelCase();     
       object[] descriptions =
         field.GetCustomAttributes(typeof(DescriptionAttribute),true);
        
       if(descriptions.Length > 0)
       {
          //just use the first one.
          guiContent.tooltip =
             (descriptions[0] asDescriptionAttribute).Description;
       }
        
       returnguiContent;
    }
     


    注意:它在类代码中使用注释,来在检查面板中产生工具提示

    3.定义新的自定义编辑器

    不幸的是,你仍然需要定义每个MonoBehaviour类。幸运的是,这些定义可以是空的;所有的实际工作都由基类来完成。

    1
    2
    3
    [CustomEditor(typeof(MyClass))]
    public classMyClassEditor : BaseEditor<MyClass>
    {}
    理论上说这一步可以自动化,但是我还没有尝试过。


    原文链接:http://devmag.org.za/2012/07/12/50-tips-for-working-with-unity-best-practices/

0 0