问题背景

问题背景

2026年起,AI越来越厉害,以前都是手写代码,后来有网页版早期的ChatGPT和Deepseek,开始资讯AI辅助写代码,最多就是复制AI给出的片段,贴到本地再适应本地项目。

而2026年后,完全可以让AI直接写Unity的或者其他引擎的代码了,甚至MCP可以直接帮你在Unity里编译好,完整debug后再交付给你,非常爽。

但是,我们团队开发一段时间后,发现了一个以前手写代码不会有的问题,那就是出现各种编码和行尾不一致的问题,一旦提交git或者svn,就会发现无缘故多了很多明明没改但提示变了的文件。这些误报会消耗很多时间精力去筛查,非常不值得。

调查情况

调查情况

先看Unity这边,由于一般是用的VS来写的代码,的行尾和编码由编辑器决定的,默认行尾是CRLF,编码格式是UTF-8带BOM。但是AI编写的代码, 就由AI模型的习惯决定了,根据训练资料有关,大多LF和UTF8无BOM。

打开网易新闻 查看精彩图片
打开网易新闻 查看精彩图片

最大坑点是,有可能AI写好的代码,你用Visual Studio打开,不小心就会自动按照VS的默认设置改掉了编码和行尾,相反AI写的也可能会改掉原有文件的编码和行尾,来回变化,导致查看差异时很多误报。

到底有无BOM两者有什么区别

到底有无BOM两者有什么区别

打开网易新闻 查看精彩图片

虽然都是UTF8,但是缺又不完全一样,到底什么区别,因此我也资讯了下AI答案:

BOM 是文件开头的三个字节,EF BB BF。它原本是给「不知道这是 UTF-8」的老工具打的一个标记。

C# 在 Windows 上的默认行为,是存成 UTF-8 带 BOM,有些配置下干脆是系统本地编码,中文 Windows 就是 GBK。

带 BOM 编译没问题,.NET 编译器认识它。麻烦出在别处。多出的三个字节会让某些 diff 工具把第一行算成改过。Linux 上的一些脚本、模板引擎、拼接工具会把 BOM 当成内容,输出里冒出一个看不见的字符。某些 JSON 解析器和字符串比较对 BOM 敏感。最烦的是抖动,同一个文件在「带 BOM」和「不带 BOM」的两台机器之间来回转,diff 会一直抖。

那到底统一成什么比较好呢?

那到底统一成什么比较好呢?

Unity 的文档里提到,含非 ASCII 字符的脚本建议用带 BOM 的 UTF-8,保证各种环境都能正确识别编码。而 AI 生成的代码天然不带 BOM。一个主要靠 AI 写代码的项目,两边的默认值正好相反。

打开网易新闻 查看精彩图片

我其实一开始挺犹豫,让AI扫了我当前的项目编码情况,带和不带各站一半就很尴尬,特别是含中文注释的UTF8无BOM不知道有没有风险。

打开网易新闻 查看精彩图片

不过根据AI提供的资讯,带BOM主要用于老工具的识别,目前可能也用不到,无论带和不带,现在单独打开编辑器看中文注释都是没问题的。

项目往后主要靠AI写代码的话,还是应该顺着AI的默认值走,少一层来回转换的必要。所以我选择了B方案。

再看行尾的问题

再看行尾的问题

AI 天然就是 LF,而Windows手写的默认是CRLF。不过我让AI实际扫描我当前项目,LF已成事实主流了。

打开网易新闻 查看精彩图片

因为同样的理由,未来会选择用AI写代码为主,所以顺应AI主流直接用LF作为规范统一是最合适的选择了。

打开网易新闻 查看精彩图片

约定成写入规范

约定成写入规范

一次性的整改只是临时的,要长期规范要真正写入成一个规范文件根治才行,AI检查后说我治理机制全缺,让我写这3个规范文件:

.editorconfig /.gitattributes /svn:eol-style

打开网易新闻 查看精彩图片

说实话以前手写代码的时候,我根本没见过这几个东西,从来没用过也能跑。但根据AI给出的资讯,其实他们在AI时代很重要,以下是AI给出的介绍:

.editorconfig卡在编辑器这一端。你新建或保存一个文件时,编辑器会先读它,按里面的规定决定用什么缩进、什么字符集、什么行尾。它是一份写给编辑器看的约定。

.gitattributes和svn:eol-style卡在版本控制这一端。文件入库的那一刻,它们负责把行尾和文本类型归一化。它们不关心你用什么工具写,只认提交这个动作。

最后一道是提交前检查,pre-commit 或者 CI。前两道都设好了,还是可能有人或者某个工具绕过去,那就靠它把漏网的文件拦下来。

最简单的文件.editorconfig

最简单的文件.editorconfig

在项目根目录放一个.editorconfig,写这么几行就够了。

root = true

charset = utf-8

end_of_line = lf

insert_final_newline = true

trim_trailing_whitespace = true

indent_style = space

indent_size = 4

[*.{json,yml,yaml}]

indent_size = 2

[*.md] trim_trailing_whitespace = false

root = true表示这是最顶层,编辑器往上找到这里就停,不再往父目录找。

charset写utf-8,注意不是utf-8-bom。这一条就是「编码用 UTF-8、不带 BOM」的写法。

end_of_line写lf,对应「行尾用 LF」。

几种主流编辑器都原生读它,VS Code、Rider、Visual Studio 2022 都不用装插件。写一次,全项目生效,没有运行时成本。

放哪儿也简单。项目根目录最省事。如果只想管住 Unity 工程,放在Assets/下面也完全安全,Unity 会忽略点开头的文件,不会给它生成.meta,也不会当资源导入。

还有个容易忽略的点。VS 和 Rider 都是启动时读它,改完记得重开一次编辑器,不重开不生效。

它的弱点也很明确。它不强制。只在编辑器愿意遵守的时候才生效。你用命令行工具直接写文件、用脚本批量生成文件,或者让某个工具直接落盘,它一律管不着。

Git版本控制的.gitattributes

Git版本控制的.gitattributes

如果你用 Git,在仓库根目录放一个.gitattributes。

* text=auto eol=lf

*.cs text eol=lf

*.sln text eol=crlf

*.bat text eol=crlf

*.cmd text eol=crlf

*.png binary

*.dll binary

* text=auto eol=lf这一行是现在的推荐起手式。git 提交时把文本文件的行尾统一成 LF 存进仓库,检出到工作区也按 eol 走。仓库里永远只有一种行尾,跨平台协作不会互相覆盖。

下面几行是明确点名。.cs走 LF,.sln和 Windows 批处理按各自的要求单独给 CRLF,图片和 dll 直接标成 binary,别让 git 去猜。

SVN属性的svn:eol-style

SVN属性的svn:eol-style

SVN 没有 .gitattributes 那种放在仓库里、人人可见的声明文件。它的做法是给文件挂一个属性,属性名就叫svn:eol-style。

先分清楚几个值,选错了效果完全不一样。

native,仓库统一存 LF,检出时按平台转换。Windows 上检出成 CRLF,Linux 和 Mac 检出成 LF。适合「每个平台用自己习惯的行尾」的团队。

LF,仓库存 LF,检出也永远是 LF,在 Windows 上也是 LF。适合「行尾就统一成 LF,谁也别转」的团队。

我要的是后一种,所以选 LF。方向定了就是全项目 LF,不要再让平台转换插一脚。

怎么设。四种方式,从最直接的到最省事的。

方式一,图形界面。在要处理的文件夹上右键,TortoiseSVN,Properties,New,eol-style,选 LF,勾上「Set property recursively」,确定。适合只改一两个目录。

方式二,命令行按目录递归。一行就够。

svn propset svn:eol-style LF --recursive Assets

svn propset svn:eol-style LF --recursive .

这里有个容易误解的点。SVN 的属性不继承。--recursive是把每个文件挨个设一遍,不是「给文件夹设了,子文件就自动有」。文件夹上挂属性,不会传给里面的文件。

方式三,命令行按清单批量。如果只想处理某类文件,还要避开第三方目录,先生成一份清单再喂给它。

svn list -R > all-files.txt

svn propset svn:eol-style LF --targets cs-list.txt

第一行把受版本控制的文件全列出来。编辑这份清单,只留 .cs,把Plugins/下面的删掉,存成 cs-list.txt。--targets后面就是一个一行一个路径的文本文件。我就是这么只给版本化的 .cs 设的属性,第三方插件一个没动,别人的代码别去改。

方式四,auto-props,让新文件自动带上。前面三种只管现有文件,之后新加的文件属性还是空的。在本机的 Subversion config 里加两段,新文件一svn add就自动带上。

[miscellany]

enable-auto-props = yes

[auto-props]

*.cs = svn:eol-style=LF

*.csproj = svn:eol-style=LF

*.bat = svn:eol-style=CRLF

*.cmd = svn:eol-style=CRLF

config 文件在 Windows 上一般在%APPDATA%\Subversion\config。改完对之后新加的文件生效。

这里有两个坑。一是enable-auto-props = yes不打开,下面的 auto-props 根本不生效,这个最常见。二是 auto-props 只对之后新增的文件起作用,已经入库的老文件得靠前面三种方式手动补。

设完别忘了提交。propset 只改了你的工作副本,属性本身也是受版本控制的。要让团队其他人都拿到,得svn commit一次。

怎么确认真的设上了。查单个文件,用 propget 加属性名和文件路径。想给整个目录体检,加--recursive,它会逐个列出路径和属性值,没设的显示 none。

svn propget svn:eol-style Assets/Scripts/Player.cs

svn propget svn:eol-style --recursive Assets

顺手再看一眼svn status。属性变更和内容变更在输出里占的是两列,别搞混。

MM Assets/Scripts/Player.cs

M Assets/Scripts/Player.cs

第一列是内容状态,第二列是属性状态。MM 是内容和属性都变了。前面空一格、只有一个 M 的,是只改了属性。我第一次看的时候就没分清这两列。

设错了想撤销,把 propset 换成 propdel,一行删掉。

svn propdel svn:eol-style --recursive Assets

改成 LF 之后,SVN 还能 diff 出差异吗

改成 LF 之后,SVN 还能 diff 出差异吗

这是我最关心的问题。我先拿一个文件做了次受控实验,结论分两层。

打开网易新闻 查看精彩图片

第一层,只改行尾,SVN 会不会认为文件变了。答案取决于有没有设 svn:eol-style。

没设的时候,SVN 是按行比较的,行尾符号也算在这一行里。你把整份文件的 CRLF 换成 LF,在它眼里就是每一行都变了,diff 变成整份删除加整份重新添加。几百行假差异,真正改的那几行得在噪声里捞。

设了之后,SVN 比较的是归一化之后的内容。纯行尾的变化被它吃掉了,不再产生 diff。只有你真正改过的行才会出现。

所以加 svn:eol-style 最大的好处,不是「能 diff」,而是让 diff 干净。它把行尾这一类噪声永久性地从 diff 里删掉了。

第二层,那设置属性本身,会不会造成一次全文件改动。可能会,而且这是必须付的一次性代价。

如果你的文件在仓库里存的是 CRLF,属性却要求 LF,那这次提交会让整个文件看起来被改了。这是一次性代价,吃掉它就好。

正确的做法是三步。第一步,把要改的文件挑出来,设属性、顺手把内容归一化。第二步,就这一次,单独提交,提交信息写明「纯格式变更,无逻辑改动」。第三步,之后正常写代码,diff 里只剩你真正动过的那几行。

关键在第二步。格式变更必须单独一条提交,不能和功能改动混在一起。混在一起,你以后想单独回滚这次格式变更都做不到。

总结经验

总结经验

在AI时代里,编码和行尾的变化问题是值得重视的,与手写时代的默认值不同了。

非常建议加上.editorconfig /.gitattributes /svn:eol-style这3个设置,至于怎么写怎么加,找AI搞就行,只不过你不说,AI就不会去做。