Unity编码规范

本文最后更新于 2026年4月13日 晚上

Unity编码规范

结构规范

让最常用的成员排在前列,让最相关的成员排在一起,便于快速索引有效信息和提示成员功能。

确定类成员的布局方式

  1. 按访问修饰符排序:公开->保护->私有[SerializeField]->私有
  2. 按是否为静态排序:静态->非静态
  3. 按具体类型排序:数据定义->事件->字段->属性->函数
  4. 按逻辑关系排序:Unity事件、成对函数等

示例:

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
public class Door
{
public enum State
{
Closing,
Opening,
Opened
}

public event Action Opened;
public event Action Closed;

public State CurrentState => currentState;
public Func<bool> OpenCondition => openCondition;

public Task OpenAsync() { throw new NotImplementedException(); }
public Task CloseAsync() { throw new NotImplementedException(); }

[SerializeField] AudioSource audioSource;
[SerializeField] AudioClip openSound;
[SerializeField] AudioClip closeSound;

State currentState;
Func<bool> openCondition;

void Awake()
{
throw new NotImplementedException(); //初始化
}

void PlayOpenAnimation() { throw new NotImplementedException(); }
void PlayCloseAnimation() { throw new NotImplementedException(); }
}

命名规范

增强可读性,避免形式主义,追求美观简洁。

  • 统一采用英语命名,不要用汉语拼音。
1
2
ShiZi //错误(必须人工解读,但没有声调和上下文,难以正确解读。石子、狮子、柿子?)
Persimmon //正确(放入翻译软件中即可得为:柿子)
  • 私有成员全部采用“驼峰命名法”
  • 非私有成员全部采用“帕斯卡命名法”(首字母大写的驼峰命名法)
  • 确保利用驼峰分割名称后,可从翻译软件中得到对应中文。
1
2
3
public State CurrentStateOfDoor { get;private set}; //正确(公开成员)
State currentStateOfDoor; //正确(私有成员)
//按驼峰分割后“Current State Of Door”可在翻译软件中解读为“门的现状”,正确。
  • 名称怕短不怕长,除非是 html 这类事实标准,否则不要缩写!
  • 进行缩写时,对缩写单次采用首字母大写,其余小写的形式。
  • 不要使用纯简单的字母、数字等无意义名称,名称必须能自注释。
1
2
3
4
5
string AssetBundleName; //正确
string AbName; //错误(Ab不是标准缩写,也无法被翻译软件解读)
string DbName; //正确(Db是标准缩写,可以被翻译软件解读为“数据库名称”)
int A,B,C; //错误(完全无法从名称中解读出该成员的含义作用)
for(int i)//正确(在当前上下文属于事实标准写法,i为index的缩写)
  • 不要故意使用匈牙利命名法(添加多余的前后缀),现代IDE能区分类型信息。(单纯是为了避免重名等需求,不受影响)
  • 不要使用下划线,有了驼峰命名法,没有下划线也能分割内容。
1
2
3
4
string userName;
Text userNameText; //错误(不用写明类型,如果这成了规范,会成为一种无意义的负担)
Text userNameUI;//正确(不仅也避免了和userName重名,而且解释了其作用)
Text _user_Name;//错误(同样是无意义的负担,而且看起来很不规整)
  • 针对具体类型有一些专门的命名要求
类型 描述 示例
接口 对应类名添加I前缀。 IInterface
泛型 对应类型添加T前缀。 TGeneric
函数 表示动作,采用主动语态。 OpenDoor
事件 表示动作,采用被动语态。
触发在对应函数开头或期间使用进行时,触发在函数结束使用过去时。
DoorOpeningDoorOpened
事件函数(用于实现事件的函数) 表示动作,采用被动语态,在对应事件命名的基础上添加on前缀。 OnDoorOpeningOnDoorOpened
字段 名词和可选的形容词,采用被动语态。由于字段不允许公开,所以永远为小驼峰命名法。 isDoorOpening
属性 通常与字段对应,故与对应字段同名。且通常属性是公开类型,故一般会采用大写开头。 IsDoorOpening
  • 针对具体功能也有些专门的命名要求(这些要求不分类型,字段名、类名等都要遵守):
功能 要求
静态单例对象 Instance
提供功能服务的类(MVP架构中的M) +System
负责界面相关的类(对应MVP中的V) +UI
负责驱动链接模块,推动游戏流程的类(对应MVP中的P) +Controller
用于实例化的预制体 +Prefab

实现规范

使用优秀的代码设计策略,增加程序的可维护性。

功能性要求

这些要求会影响你的功能实现,但有助于不同功能间的开发协作。

  • 非常量字段永远不允许公开,应使用属性代替。
1
2
public int Value;//错误(字段操作无法监控,外部程序可以随意操控)
public int Value {get; private set;};//正确(属性可以验证操作过程,避免错误行为)
  • 基于[SerializeField](或[RequireComponent])实现依赖注入,而非不受控的依赖获取。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class {
public void Play(){
AudioSource audioSource = GetComponent<AudioSource>()
audioSource.Play();
}
} //错误(无法确保外部环境正确,也无法提醒用户配置)

class {
public void Play(){
audioSource.Play();
}
[SerializeField] AudioSource audioSource;
} //正确(外部用户将会在面板上看到依赖的对象的槽位)

[RequireComponent(typeof(AudioSource))]
class {
public void Play(){
AudioSource audioSource = GetComponent<AudioSource>()
audioSource.Play();
}
} //正确(附着的物体将会被强制添加上依赖组件)
  • Awake(或OnEnable)阶段完成功能模块初始化,存在依赖时配合[DefaultExecutionOrder]控制。
  • 不要基于构造函数、析构函数等C#事件处理数据,应全部在Unity事件中处理,否则部分Unity功能会无法使用。
  • 尽可能禁用编辑器选项 Reload DomainReload Scene,从而加快编辑器运行速度,提高开发迭代效率。
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
[DefaultExecutionOrder(-10)]//正确(SeedGenerator的初始化时机被显式指定,便于其他模块使用)
class SeedGenerator {
public int GetSeed() => currentSeed;
const int defaultSeed = 1;
//错误(不允许依赖构造函数等初始化,部分情况下不能正常触发)
int lastSeed = 9;
int currentSeed = defaultSeed;
void Awake(){
//兜底初始化
currentSeed = defaultSeed;//正确(在Awake阶段时确保模块被初始化(可用))
}
void OnEnable(){
//完全初始化
currentSeed = lastSeed;//正确(基于Unity事件处理数据)
}
void OnDisable(){
//资源回收
currentSeed = -1;//正确(基于Unity事件处理数据)
}
}
class Random {
[SerializeField] A a;
int seed;
void Awake(){
//依赖A的初始化
seed = a.GetSeed();//正确(由顺序被确定,此时a已经执行OnEnable(或Awake),可以使用)
}
}

可读性要求

这些要求不会影响功能和程序设计,但可以使代码美观,便于读写。

  • 省略默认代码(如私有访问修饰符),让系统自动提供,从而简化代码减少阅读量。
1
2
3
4
5
6
//错误(显式写明修饰符较冗长且需要专门阅读修饰符的单词内容)
private int value;
public int Value {public get;public set;}
//正确(非常简约,大幅减少代码量,且不需要专门阅读修饰符)
int value;
public int Value {get;set;}
  • 区分“取反”和“判否”,在判否时显式写明为与否定的比较。
1
2
3
4
if(isDoorOpened == false)//正确
if(isDoorOpened)//正确(省略默认代码,减少阅读量)
if(!isDoorOpened)//错误(用取反符号判断容易看漏,而且无法显式表达出判否的意图)
bool isDoorClosing = !isDoorOpened;//正确(按取反的意图使用取反符号)
  • 避免使用 var,应显式写明类型信息。
1
2
3
var a = GetValue(); //错误(必须通过二次查询才能获取类型信息,非常影响阅读效率)
int a = GetValue(); //正确(一眼就可以看出类型信息,非常直观,清晰的类型名易于理解代码)
List<int> b = new();//正确(最新的语法糖,保留类型信息同时简化代码,比var好)
  • 当有多个if判断时,使用基于级联的布局方式,而非嵌套。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Func(){
if(){
if(){
if(){
}
}
}
}//错误(嵌套布局,代码块重合,不满足开闭原则,可维护性差)
void Func(){
if(){}
return;
if(){}
return;
if(){}
return;
}//正确(各代码块相互独立,不容易误操作,且功能独立易于理解)
  • 代码中的任何元素(变量函数等)应保持最小作用域,仅放在确实要用的环境中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//错误(PreProcess仅在Process使用,但放在外部作用域反而会成为读者的干扰项(误以为是有用的独立函数))
void PreProcess(){};
//错误(同 PreProcess,而且其命名“i”在定义环境下无法向读者解释含义,存在命名错误)
int i;
void Process(){
PreProcess();
for(i){}
}

void Process(){
//正确(只有阅读Process的读者,才需要知道PreProcess和processStep的实际功能)
void PreProcess(){};
int processStep;

PreProcess();
for(processStep){}
}
  • 优先使用新式的C#语法糖和功能,减少代码量,统一实现方式。
1
2
3
4
5
6
7
8
//示意例子:
Action -> await / async
if(a is null) -> ?.
a == null ? a:b => ??
for/foreach -> Linq
class{} -> var=new {}
int i,j,k;k=j;j=i;i=k -> (i,j) = (j,i)
.....等等

注意:部分新式写法存在性能和兼容性问题,例如:Linq 可能产生 GC;空条件运算符对Unity对象无效。故要根据实际情况选择性使用,甚至在性能密集场合,手写的排序比 List.Sort 更高效(因为Sort也有GC)。

如果偷懒不想阅读 C# 文档,建议使用一下 Rider IDE,远比 VS 等在代码提示和检查上智能的多(毕竟以前都没有免费版),用一遍可以学到很多知识。


Unity编码规范
https://bdffzi-blog.pages.dev/posts/4118079887.html
作者
BDFFZI
发布于
2026年1月29日
许可协议