UVP 价值专家 | 主程必杀技:Unity C# 代码整洁之道(一)

文摘   游戏   2024-05-11 19:35   上海  
这篇文章来自 2024 年度 Unity 价值专家提名人选 Laugh。Laugh 专注游戏研发及管理 15 年,目前在巨人网络担任技术负责人、专家,负责 MMO 类型的架构设计、产线设计及团队建设。他在 Unity 官方开发者社区发布了“主程必杀技”系列文章,探索使用 Source Generator 和 Analyzer 在 Unity 中实现代码规范框架要求的方式,帮助主程更高效地进行技术管理。
点击阅读原文,可以访问 Laugh 在 Unity 官方开发者社区的个人主页,阅读更多技术干货。

作为主程的你,大概率为 Review 烦恼过:花大量的时间来检查小伙伴的代码是否符合代码规范和框架要求。
项目开始时,主程和核心人员大都会先设计框架、制定各种规范,其中比较重要的一项就是制定代码规范。在前期团队人员较少的时候,代码规范还能较好的执行,通过代码扫描工具、代码质量控制工具、以及 Review 来保证开发人员的代码符合规范。然而,随着团队人员的增加、开发节奏的加快,Review 成了负担。而且,想通过工具来检查代码是否符合框架要求还不太现实,很难找到相应的工具。  
很多年前,我曾写过一个工具,自定义一些规则来检查代码是否符合规范和框架,带能实现的功能有限,而且不能实现语法层面的分析。直到前年,我在研究 DOTS 时,发现其用了一个很好的 C# 特性:Roslyn。查看文档后,发现 Unity 很早就支持 Source Generator 和 Analyzer。
经过一段时间的项目实战,使用了代码生成(Source Generator)Analyzer。其中 Souce Generator 主要用来辅助框架的搭建,Analyzer 主要用来框架规范的检查,这些都是在 precompile 阶段完成的,不符合要求的代码会直接报 Error 且无法编译。可以粗略的认为是实现了自定义语法。  
本系列文章,我将通过实例的形式和大家一起探讨学习 Roslyn 在代码规范和框架搭建上的应用。

演示代码

获取链接:https://gitee.com/palang/unity-sharp-code-regulator.git
C# Code Analyzer 入门系列文章
 Analyzer 初体验:强制使用大括号包括逻辑分支 (if...else...)
 强制加注释(上)类注释 | 强制加注释(下)方法注释
 命名规范检查(一)命名空间和类名
 命名规范检查(二)public和private方法名称规范检查
 命名规范检查(三)成员变量命名规范检查
 命名规范检查(四)临时变量命名检查
 检查继承关系
本文将率先为大家带来该系列第一篇《Analyzer 初体验:强制使用大括号包括逻辑分支 (if...else...)》,系列其他文章将每周更新,或可访问 Laugh 的 Unity 官方开发者社区主页,持续关注学习。

Analyzer 初体验

强制使用大括号包括逻辑分支 (if...else...)

由于编程习惯的不同,在写 if...else... 逻辑分支时,如果只有一行代码,有人习惯使用 {} 包括,有人却习惯不用(特别是新手)。但经验上来看,加 {} 不但可读性强,而且有益于维护,特别适用于不同的人维护同一段代码、一行变多行的情况。
希望实现的效果:Unity 中,强制使用“推荐的写法”,即添加花括号 {}。
不推荐的写法:
int a = 0;if(a > 0)    a ++;else    a --;
推荐的写法:
int a = 0;if(a > 0){    a ++;}else{    a --;}

一、开发 Analyzer

  1. 准备环境

工具使用 Visual Studio 2022 Community,搜索下载安装即可。

⦁ 使用 VS 新建类库项目

⦁ 类库框架选择 .Net Standard 2.0 (兼容 Unity)

⦁ 添加 Roslyn 包:搜索找到 Microsoft.CodeAnalysis.CSharp,安装 3.8.0 版本。安装成功后,依赖项中会出现“分类器”。

创建 AnalyzerReleases.Unshipped.md 文件,以便后面添加 analyzer release ID。

  2. 新建分析器
 新建常量类 DianogsticIDs 类,用来定义错误 ID 常量
⦁ 新建 DiagnosticCategories 类,用来定义

using System.Collections.Generic;using System.Text;
namespace CdeAnalyzer{ public static class DianogsticIDs { public const string FORCE_BRACE_ID = "F-ERR1001"; } public class DiagnosticCategories { public const string Criterion = "Criterion"; }}

排除目录定义(配置)ConstraintDefinition.cs

using System;using System.Collections.Generic;using System.Text;
namespace Analyzer{ public class ConstraintDefinition { public static List<string> AnalyzerExcludePath = new List<string>() { "/PackageCache/", "/ThirdLibs/", "/Plugins/" };
public static bool ExcludeAnalize(string path) { foreach(var file in AnalyzerExcludePath) { if(path.Contains(file)) { return true; } } return false; } }}

修改 AnalyzerReleases.Unshipped.md 文件如下

新建类文件 ForceStatementBrace.cs,代码如下

using Microsoft.CodeAnalysis;using Microsoft.CodeAnalysis;using Microsoft.CodeAnalysis.CSharp.Syntax;using Microsoft.CodeAnalysis.Diagnostics;using System.Collections.Generic;using System.Collections.Immutable;using System.Linq;/** * Author: Laugh(笑微) * https://developer.unity.cn/projects/65937455edbc2a001cbd8102 */namespace Analyzer{    [DiagnosticAnalyzer(LanguageNames.CSharp)]    internal class ForceStatementBrace : DiagnosticAnalyzer    {        private static readonly DiagnosticDescriptor FroceBraceDescriptor =            new DiagnosticDescriptor(                DianogsticIDs.FORCE_BRACE_ID,          // ID                "逻辑分支必须使用花括号包括",    // Title                "逻辑分支必须使用花括号包括", // Message format                DiagnosticCategories.Criterion,                // Category                DiagnosticSeverity.Error, // Severity                isEnabledByDefault: true    // Enabled by default            );        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(FroceBraceDescriptor);
public override void Initialize(AnalysisContext context) { context.RegisterSyntaxTreeAction(AnalyzeSymbol); } private static void AnalyzeSymbol(SyntaxTreeAnalysisContext context) { var root = context.Tree.GetRoot(context.CancellationToken); List<ExpressionStatementSyntax> reportedNode = new List<ExpressionStatementSyntax>(); foreach (var ifstatement in root.DescendantNodes()?.OfType<IfStatementSyntax>()) { var expressStatements = ifstatement.DescendantNodes()?.OfType<ExpressionStatementSyntax>().ToList() ; foreach(var estate in expressStatements) { if (ConstraintDefinition.ExcludeAnalize(context.Tree.FilePath)) {//排除特殊目录 return; } if (reportedNode.Contains(estate)) { continue; } if(!(estate.Parent is BlockSyntax)) { reportedNode.Add(estate); var diagnostic = Diagnostic.Create(FroceBraceDescriptor, estate.GetFirstToken().GetLocation()); context.ReportDiagnostic(diagnostic); } }
} } }}
  3. 编译该项目,生成 dll 文件备用 (debug)

二、在 Unity 中使用

  1. Unity 工程

新建 Unity 项目

Assets 下新建 Scripts 目录

把之前生成的 dll 文件放入 Assets/Scripts 目录下

Unity project 出口中找到该 dll,点击选中,在 Inspector 设置如图:

  2. 测试脚本

新建脚本 TestAnalyzer.cs,并添加测试脚本

using System.Collections;using System.Collections.Generic;using UnityEngine;
public class TestAnalyzer : MonoBehaviour{ // Start is called before the first frame update void Start() { int a = 0; if (a == 0) a = 1; else a = 2; }
// Update is called once per frame void Update() {
}}

这时可以看到 Unity Console 窗口中有两行报错

代码如有不足或不优雅之处,望同仁们不吝赐教。


下周将带来 Laugh 的“主程必杀技” Unity C# 代码整洁之道系列文章第二辑《强制加注释》,敬请期待~
Laugh 是 2024 年度 Unity 价值专家提名人选。Unity 价值专家(UVP)是通过原创作品启发国内创作者的 Unity 专业人员,点击这里提名/自荐
长按关注
Unity 官方开发者服务平台
第一时间了解 Unity 社区动向,学习开发技巧

 点击“阅读原文”,访问 Laugh 的社区主页 



Unity官方开发者服务平台
Unity引擎官方开发者服务平台,分享技术干货、学习课程、产品信息、前沿案例、活动资讯、直播信息等内容。
 最新文章