Press "Enter" to skip to content

Unity 项目文件夹结构及命名规范

内容纲要

原文:http://www.arreverie.com/blogs/unity3d-best-practices-folder-structure-source-control/

前言

Unity3D 本身对项目文件夹没有特别硬性的规定, 但是良好的文件夹结构, 以及命名规范对于后续的开发, 管理, 工具的开发维护至关重要, 这里给出Unity Project structure 一般规范和原则

原则

  1. 避免多个版本的assets : 任何asset都应该只有一个版本。如果必须要多个版本的Prefabs, scene或 模型,一定要在名字上做明确的区分。例如,使用双下划线前缀:__MainScene_Backup
  2. 每个项目组成员应当在自己的分支下工作
  3. 尽量保持"短小精悍" : 尽量保持代码的简洁和精准, 尽量保持每个类, function 专注于相对单一的功能
class 建议行数
单一功能 class 100 行以内
中型 class 400 行以内
主 class 1000 行以内
  1. 可以考虑使用xml来保存场景: 便于代码合并

文件夹结构及场景组织结构

Unity 在文件夹结构组织方面非常的自由, 每个公司,甚至每个项目都会有不同,以下是一个较为通用的3D项目文件夹结构, 可以根据自己的项目进行调整和裁剪

  |-- Assets
        |-- Animations          // 动画
        |   |-- 2DAnimations    // 2D动画
        |   |-- 3DAnimations    // 3D动画
        |-- Editor              // **特殊意义**, 与工具扩展Editor有关
        |-- Media               // 媒体文件
        |   |-- Audio
        |   |-- Video
        |-- Art                 // Art部门导入的模型,贴图等
        |-- Materials           // 材质文件
        |-- Plugins             // **特殊意义**, 插件
        |   |-- Android
        |   |-- iOS
        |   |-- Special
        |-- Prefabs             //Prefabs
        |-- Resources           // **特殊意义**
        |   |-- Android
        |   |-- iOS
        |   |-- Common
        |-- Scenes              // 场景文件
        |   |-- Levels          // 不同的关卡
        |   |-- Others
        |-- Scripts             // 脚本
        |   |-- Editor          // 与Editor先关的脚本
        |-- Shaders             // 着色器文件夹
        |-- StreamingAssets     // **特殊意义**
        |-- SandBox             // 沙盒文件夹
        |-- Sprites             // 粒子特效

关于Streaming Assets:

大部分Unity的Asset在build的时候都会被合并到项目中. 不过有时候需要将一些文件放到系统的某个路径下面,以便在运行的时候访问. 一个例子就是ios播放movie文; 在ios下面如果要使用Playmovie来播放movie文件的话, 就必须要使用系统级的路径, 这时就需要使用Streaming Assets.

在StreamingAssets文件夹中的内容将被逐一拷贝到特定的文件夹中. 你可以通过StreamingAssetsPath属性来访问. 最好是用Application.streamingAssetPath来得到StreamingAssets的本地路径.

在不同的平台, 该路径地址不尽相同:\

系统 路径
mac / pc path = Application.dataPath + "/StreamingAssets";
ios path = Application.dataPath + "/Raw";
Android path = "jar:file://" + Application.dataPath + "!/assets/";

说明

  1. 路径不要用空格: 无论是文件路径还是文件名,都不要含有空格 Untiy 命令行工具可能无法自动支持带有空格的路径
  2. 不要在Asset根目录存放任何单独asset文件
  3. 一旦项目建立了, 避免在根目录创建额外的文件夹: 除非团队成员达成共识
  4. 命名规则一旦确定尽量保持一致. 推荐"驼峰命名法

场景结构

以下是场景结构参考, 我们可以根据项目实际情况进行调整

  • Lights
  • Cameras
  • World
  • GUI
  • Scene_Manager
  • _Dynamic

注意:

1. 使用空的物体作为场景文件夹: 所有空的物体都应当保持transform 是归零的, 即上述的结构中的根组,保持transform 归零

2. 动态生成的物体请放到"_Dynamic"中

3. prefabs 和 空的folder 建议保持transform归零:除了用来定位用的transform外,其他应当尽量归零。这样会减少局部空间和世界坐标之间转换的问题, 维护也会更加简单。

4. 将Character或者站立物体的pivots放在Base(地面上)而不是放在中心点: 这个对于3D项目来说, 更容易将角色定位到地面上

5. 保持朝向一致 : 保持模型的朝向一直,比如 正/负Z轴, 一旦确定了,整个项目都需要保持统一, 很多算法和机制在朝向一致的情况下会更简单

6. 比例非常重要, 在一起开始就确定好 : 尽量确保所有的物体在初始的时候scale都是1, 在项目之初确定要单位比例, 并始终保持如一

7. 确保每个场景都可以跑的通 : 这将大大的减少测试时间. 你需要做两件事:首先,提供一种方法来模拟之前加载的场景所需要的数据(如果这些数据不可用的话)。第二,刷出对象,这些对象必须在场景加载之间保持如下的习惯用法:

myObject = FindMyObjectInScene();
if (myObjet == null)
{ myObject = SpawnMyObject(); }

使用Git 对Unity进行版本控制

基本设置:

虽然Untiy项目有很多文件夹, 但其实只有三个文件夹是需要进行版本控制的: Assets, Packages, ProjectSettings. 其他文件夹都可以从以上三个文件夹生成

文件夹 说明
Assets 存放了项目所有的游戏资源, 包括脚本, 贴图, 声音, 自定的编辑器扩展等, 项目中最终要的文件夹, 没有之一
Library 导入Asset时的本地Cache文件夹, 当版本控制的时候应当被忽略
Packages 包含了一系列描述包依赖关系的json文件
ProjectSettings 项目配置文件, 包括物理, tags, Player settings等等, 简单说就是所有你可以在Project settings 里面设置的内容都在这个文件夹中
Temp 顾名思义临时文件夹, 当编译的时候会自动生成, 原来是MonoDevelop 使用,后来Unity也会使用
ProjectName.sln VS的项目解决方案文件

这里给一个gitignore 的unity 通用模板:
https://github.com/github/gitignore/blob/master/Unity.gitignore

在版本控制之前, 首先要做一些必要的设置:

  • 将Meta files 设置为可见文件 Edit → Project Settings → Editor → Version Control Mode → Visible Meta Files.
  • 将Asset 序列化设置为text模式: Edit → Project Settings → Editor → Asset Serialization Mode

可以使用VCS的Gui工具,例如 sourcetree,或者直接使用git 的命令行模式进行版本控制操作, 常见的命令有:

命令 作用
git init 初始化仓库
git clone 克隆已有的仓库
git status 查看当前仓库状态
git add 将修改暂存
git commit 提交commit
git push push commit
git pull 拉取 commit

预制件

预制件是Unity 非常重要的一项功能, 预制件的使用有以下一些原则:

1. 尽量场景中的物体全部用预制件

2. 用独立的预制件来实现特殊性, 尽量不要在场景中对预制件实例做特殊化
例如如果你有两种enemy类型, 而且他们仅仅是一些属性不同的话, 就把他们直接分成两种不同的预制件, 这样做的好处有:

  • 可以统一的对每种类型进行修改
  • 可以只是修改prefabs 而不用修改scene

3. 将prefabs 之间做链接, 而不是prefabs instance
prefabs之间的link 可以在场景中仍然保持, 而prefabs instance 之间的link则在场景中保持

4. 如果需要添加额外的脚本的话, 尽量不要把meshe组建放到prefabs的root上
当创建一个含有mesh的prefab的时候,root最好是一个空的game object. 可以把scripts 集中方到prefab的root上, mesh 放到child object 上, 这样做的好处是可以更加规范和方便的替换模型, 而不会改变其他的属性.

Class的一些原则

关于单例

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;
        }
    }
}

单例适合manager类, 例如 ParticleManager, AudioManager, GUIManager

  • 避免对非管理类(如玩家)的预制件的实例使用单例, 及时这样的实例有时候是唯一的。否则会使得承层次结构和类的继承复杂化
  • 单例的public 变量和方法尽量使用 static ,这样可以通过非实例直接进行访问, 例如GameNamager.Player 而不是 GameManager.Instance.player

对于component组件来说,进入过一个参数不需要调整, 就不要设置成public.
如果某些情况下无法避免的话, 注意给参数加上[HideInInspector]隐藏起来.

除了需要显示的text外, 尽量不要用string
尽量不要用string来识别物体或者预制件(有一个例外的情况是,访问和查询动画的时候还是会用到string)。

使用struct 或者 class 对参数和属性进行打组以便管理和修改

案例可以参考https://docs.unity3d.com/ScriptReference/Serializable.html

using System;
using UnityEngine;

public class Player : MonoBehaviour
{
    //Create a custom struct and apply [Serializable] attribute to it
    [Serializable]
    public struct PlayerStats
    {
        public int movementSpeed;
        public int hitPoints;
        public bool hasHealthPotion;
    }

    //Make the private field of our PlayerStats struct visible in the Inspector
    //by applying [SerializeField] attribute to it
    [SerializeField]
    private PlayerStats stats;
}

如果有大量的文本,对话等文字内容,尽量把这些内容方到单独的文本文件中

关于文档

重视文档及文档规范

一些关于类和功能的文档和注释, 大部分都在代码中, 但是在一些关于项目架构, 原则, 结构, 框架性的文档, 应当放在代码之外, 至少应当在Readme.md中包含关于项目重要的内容:

文档应当包含:

  • 关于项目的模块图, 流程图, 用来帮助组员快速理解项目结构
  • 关于代码和脚本规范的文档
  • 关于项目Layer的说明文档(collison, culling, raycasting 等等)
  • 关于Tags 的使用规范
  • Gui depth规范
  • Scene场景搭建规范
  • 项目中通用或常用的概念和名字解释
  • Animation layer的相关规范

命名规则

统一的命名规则和文件夹结构可以让检索变得更快捷, 更可以做到以名知用.

通用原则:

  • 命名的主体应当简单明了的描述对象是什么, 例如bird 应当命名为Bird, 不要把命名复杂化
  • 保持命名的连贯性, 一旦你选定了名字, 尽量不要修改其主体
  • 不要使用version号,或者是制作阶段,例如(WIP, FINAL), 尽量使用VCS来控制不同的版本
  • 禁用特殊符号, 如@ #等
  • 在设计文档中使用正确形态, 例如: 如果一个死亡的动画, 应当使用DarkVampire@Die(动词) 而不是DarkVampire@Death(名词);
  • 左侧形容词原则, 例如: DarkVampire(VampireDark错), PauseButton(ButtonPaused错), 这样方便在inspector中更方便的找到Pause功能的button
  • 序列命名从0开始, 如果一些位于序列的命名, 需要从0开始计数, 例如: PathNode0, PathNode1
  • 非序列命名不用数字, 如果并不是在同一序列的名字, 不要使用数字来做区分, 例如: 使用Flamingo, Eagle, Swallow 而不是(Bird0,Bird1, Bird2), 更方便从名字中直接区分物体
  • 临时物体加__前缀, 如果是临时创建的物体, 加双下划杠前缀, 例如: "__Player_Backup"
  • 避免使用_区分不同物体 ,例如: 使用SmallRock, LargeRock, 而不是 "Rock_Small, Rock_Large"
  • _下划线仅用来区分重大的不同描述属性, 例如:
    • 描述GUI 按键状态: EnterButton_Active, EnterButton_Inactive
    • 贴图: DarkVampire_Diffuse, DarkVampire_Normalmap
    • Skybox: JungleSky_Top, JungleSky_North

性能及内存使用优化

1. Cache组件查询

一个简单提高组件(component)检索效率的方法, 就是对于经常用到, 但是基本不改变的组件, 把检索结果cache出来, 例如:


using UnityEngine;
using System.Collections;
public class example : MonoBehaviour
{
    Transform thisTransform;
    public new Transform transform
    {
        get { if (thisTransform == null)
                {
                    thisTransform = base.transform;
                    return thisTransform;
                }
            }
    }
    void Update(){
        transform.Translate(0, 0, 5);
    }

}

2. 尽量使用系统内置Array类型

c#内置c# Array非常快。虽然ArrayList或Array类某些情况下更容易使用(比如你可以很容易地添加元素),但是相比内置的Array来说速度相差很多。内置数组有一个固定的大小,但大多数情况下,可以在稍后在甜宠。内置数组最好的一点是,它们直接将struct数据类型放入到固定的内存中,而不需要任何额外的Tyeps或其他开销。因此,遍历缓存非常容易,因为在内存中一切都是线性的。

using System.Collections;
public class example : MonoBehaviour
{
    private Vector3[] positions;
    void Awake()
    {
        positions = new Vector3[100];
        int i = 0;
        while (i < 100) {
            positions[i] = Vector3.zero; i++;
        }
    }
}

3.避免使用 GameObject.Find.
大部分使用GameObject.Find 的情况, 都可以使用一个SerializField来替代, 虽然需要在场景中多花一些时间设置

4. 尽量避免使用 foreach()
foreach 用起来好像很爽, 但是它会有一些额外的开销, 比如她会在遍历前,调用 GetEnumerator(), 最好是使用c++ 风格的 for(;;)

5. 避免使用string
String类型在.net中式不可变的, 你不能像使用C一样的操作他.
在使用UI中, 使用StringBuilder来build string效率会更高, 尽量在最后使用的时候再convert成string. 你可以将它作为key(类似指针)来使用

6. 多使用struct
mono中的struct类型是在堆栈上分配的,所以如果你原先有一个不会离开作用域的执行类class,那么就把它变成一个结构。注意, 结构是按值传递的,所以必须在参数前加上ref,以避免任何复制成本。

7. 尽量少用function pointer

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注