当前位置: 首页 > article >正文

【Unity3D】Unity3D学习笔记

文章目录

  • 前言
  • C#命名规范
  • C#基础知识
    • 一、C#的数据类型
      • 1.1 值类型
      • 1.2 引用类型
        • 1.2.1 字符串类型(String)
        • 1.2.2 对象类型(Object)
        • 1.2.3 动态类型(Dynamic)
      • 1.3 指针类型
    • 二、C#运算符优先级
    • 三、分支语句和循环控制语句
    • 四、数组
      • 4.1 一维数组
      • 4.2 二维数组
    • 五、自定义类型
      • 5.1 枚举类型
      • 5.2 结构体
    • 六、访问修饰符
      • 6.1 访问修饰符简介
      • 6.2 默认访问修饰符
    • 七、函数
      • 7.1 函数的基础运用
      • 7.2 ref的使用
      • 7.3 out的使用
      • 7.4 函数的重载
      • 7.5 递归函数
    • 八、类和对象
      • 8.1 类的基础运用
      • 8.2 继承
      • 8.3 多态
        • 8.3.1 静态多态性
        • 8.3.2 动态多态性
    • 九、接口
    • 十、泛型
    • 十一、委托和事件
    • 十二、异常处理
    • 十三、反射
    • 十四、特性
  • 常用数据结构
    • 一、动态数组(ArrayList和List<T>)
    • 二、栈(Stack)和队列(Queue)
    • 三、集合(HashSet和SortedSet)
    • 四、哈希表(Hashtable)和字典(Dictionary)
    • 五、堆/优先队列(PriorityQueue)
    • 六、双向链表(LinkedList)
  • System命名空间
    • 一、Console类
      • 1.1 输出语句
      • 1.2 输入语句
    • 二、Convert类
    • 三、Random类
    • 四、String类
  • System.IO命名空间
    • 一、File类
  • System.Text命名空间
    • 一、StringBuilder类
  • UnityEngine命名空间
    • 一、MonoBehaviour类
    • 二、GameObject类
    • 三、Vector3结构体
    • 四、Quaternion结构体
    • 五、Time类
    • 六、Mathf结构体
    • 七、Input类
    • 八、Object类
    • 九、Ray结构体
    • 十、GUI/GUILayout类
  • 标签属性(Attribute)
  • Unity组件
    • 一、Transform组件
    • 二、Rigidbody组件
    • 三、Collider组件
    • 四、Shadow/Outline组件
  • Shader
    • 一、基础结构和属性
      • 1.1 基础结构
      • 1.2 各种属性类型
      • 1.3 语义
      • 1.4 SubShader简介
      • 1.5 常见渲染状态设置
      • 1.6 Unity内置变换矩阵
    • 二、固定管线着色器
      • 2.1 简单说明
      • 2.2 基础语法
    • 三、表面着色器
    • 四、顶点/片元着色器
      • 4.1 基本结构
      • 4.2 顶点着色器和片元着色器之间的通信方法
      • 4.3 示例
        • 4.3.1 漫反射效果
        • 4.3.2 单张纹理
        • 4.3.3 凹凸映射
        • 4.3.4 渐变纹理
    • 五、内置文件和变量
    • 附录1:另Spine/Skeleton Shader支持透明度修改功能
  • 数据本地化技术
    • 一、BinaryFormatter
    • 二、XML
    • 三、Json
    • 四、ScriptableObject
  • 参考资料

前言

本文仅用于记录C#学习过程中的知识点,由于有一定计算机基础,所以不会面面俱到。
.NET 8文档:https://learn.microsoft.com/zh-cn/dotnet/api/system


C#命名规范

参考:C#操作手册(一):命名规范
1、【强制】类名命名规则:大驼峰命名法【DTO、POCO、VO等除外】

public class UserInfo{}

2、【强制】属性命名规则:大驼峰命名法

public string UserInfo { get; set; }

3、【强制】字段、参数、成员变量、局部变量命名规则:小驼峰命名法

public string userName;
public string GetUserName(string userId) { return "userName"; }

4、【强制】方法/函数命名规则:大驼峰命名法

public int GetUserInfo() { }

5、【强制】常量命名规则:名称全部大写,单词间用下划线_分开

public const string USER_NAME = "userinfo";

6、【强制】DTO、POCO、VO命名规则:大驼峰命名法+DTO/VO/POCO等

public class UserInfoDTO { }
public class UserInfoVO { }
public class UserInfoPOCO { }

7、【强制】命名空间命名规则:大驼峰命名法

namespace UserInfo { }

8、【强制】枚举命名规则(枚举名称采用大驼峰命名规则,枚举成员所有名称也使用大驼峰命名法)(没有特殊情况的话,枚举成员建议从默认值0开始)

public enum UserState{Success,Fail}

9、【强制】代码中所有成员禁止直接使用中文的命名方式,禁止使用中文拼音命名(一些通用的命名除外:比如城市可以采用beiJing、shangHai这样的命名规则是可以的),禁止使用中英文混合命名方式,禁止出现a、b、c、aa、ss、x、xx等毫无意义的命名方式
10、【推荐】复数类型(集合类、数组等)命名规则:优先以小写字符s结尾,如果单词最后的字母就是s或其他不适合s结尾的单词,可以使用复数类型的类型名称结尾(如List、Array等结尾)。前面规则如果都不好命名,可自行命名

public List<string> userNames { get; set; }
public string[] userNameArray { get; set; }
public List<string> userNameList { get; set; }

11、【强制】接口命名规则:以大写字母I开头+类名称

public interface IUserInfo { }

12、【强制】异常类命名规则:大驼峰命名法+Exception

public class UserInfoException { }

13、【强制】项目命名规则:大驼峰命名法,各个字母之间用字母(.)隔开

XiongZe.ProjectManagement.Services

14、【推荐】业务层和数据层名命名规则:业务层类库名称命名以Service结尾、数据层类库命名以Repository结尾


C#基础知识

一、C#的数据类型

1.1 值类型

类型描述范围默认值
bool布尔值true 或 falsefalse
byte8位无符号整数0 ~ 2550
sbyte8 位有符号整数类型-27 ~ 27 - 10
char16 位 Unicode字符U+0000 ~ U+FFFF‘\0’
decimal128 位精确的十进制值,28-29 有效位数-7.9 x 1028 ~ 7.9 x 1028 / 100 ~ 280.0M
float32 位单精度浮点型,6-7 有效位数-3.4 x 1038 ~ +3.4 x 10380.0F
double64 位双精度浮点型,15-16 有效位数±5.0 x 10-324 ~ ±1.7 x 103080.0D
short16 位有符号整数类型-215 ~ 215 - 10
int32 位有符号整数类型-231 ~ 232 - 10
long64 位有符号整数类型-263 ~ 263 - 10L
ushort16 位无符号整数类型0 ~ 216 - 10
uint32 位无符号整数类型0 ~ 232 - 10
ulong64 位无符号整数类型0 ~ 264 - 10

注:C#中bool和int之间无法直接进行转换。

1.2 引用类型

1.2.1 字符串类型(String)

字符串的定义过程中,String和string在使用过程中没有本质区别,最终都会映射到 System.String 类。

// 下面两种定义方式是等价的
string str = "abc";
String str = "abc";

在使用字符串路径过程中,若需要用到反斜杠,为方便使用,可以在前面加上@,此时不会再将反斜杠编译为转义符,类似于Python的r"x:\xxx"。当然也可以使用正斜杠,就不存在该问题了。

// 下面两种定义方式是等价的
string path = @"C:\Windows";
string path = "C:\\Windows";

@ 字符串中还可以任意换行,换行符及缩进空格都计算在字符串长度之内。类似于Python的"““xxxx””"。

// 下面两种定义方式是等价的
string str = @"aaa
bbb
ccc";
string str = "aaa\nbbb\nccc";

字符串的拼接可以采用+的方式。

// 下面两种拼接方式是等价的
string str = 5 + "5";
string str = "5" + "5";
1.2.2 对象类型(Object)

参考:object(C# 参考)
在 C# 的统一类型系统中,所有类型(预定义类型、用户定义类型、引用类型和值类型)都是直接或间接从 Object 继承的。 可以将任何类型的值赋给 object 类型的变量。

将值类型的变量转换为对象的过程称为“装箱”;将对象类型的变量转换为值类型的过程称为“取消装箱”。
在下面的示例中,整型变量num装箱并赋给对象obj。

int num = 123;
object obj = num;

然后,可以对对象obj取消装箱并将其赋值给整型变量num:

object obj = 123;
num = (int)obj;
1.2.3 动态类型(Dynamic)

用户自定义引用类型有:class、interface和delegate。

1.3 指针类型

在C#中,指针类型要在不安全的上下文中才可以使用。

二、C#运算符优先级

下表按最高优先级到最低优先级的顺序列出 C# 运算符,每行中运算符的优先级相同。

运算符类别或名称
x.y、f(x)、a[i]、x?.y、x?[y]、x++、x–、x!、new、typeof、checked、unchecked、default、nameof、delegate、sizeof、stackalloc、x->y主要
+x、-x、x、~x、++x、–x、^x、(T)x、await、&&x、*x、true 和 false一元
x…y范围
switch、with switch 和 with表达式
x * y、x / y、x % y乘法
x + y、x – y加法
x << y、x >> yShift
x < y、x > y、x <= y、x >= y、is、as关系和类型测试
x == y、x != y相等
x & y布尔逻辑 AND 或按位逻辑 AND
x ^ y布尔逻辑 XOR 或按位逻辑 XOR
x | y布尔逻辑 OR 或按位逻辑 OR
x && y条件“与”
x || y条件“或”
x ?? yNull 合并运算符
c ? t : f条件运算符
x = y、x += y、x -= y、x *= y、x /= y、x %= y、x &= y、x |= y、x ^= y、x <<= y、x >>= y、x ??= y、=>赋值和 lambda 声明

三、分支语句和循环控制语句

除去switch语句以外,与C++语法基本一致。
在C++中,switch语句可以不加break;但在C#中一定要加break。


附:foreach用法

/* 将collection当中的所有元素逐个取出, 每次取出的元素被命名为item
类似于Python当中的
for item in collection:# 循环体
但是foreach可以将二维数组当中的所有元素也逐个取出, 而不需要两层遍历
*/
foreach (var item in collection) {//循环体
}

四、数组

4.1 一维数组

动态初始化

// length个默认值的数组, 对于int, 就是length个0
int[] nums = new int[length];
// 也可以直接设定好数值, 这时长度必须为常量, 且与后面的数字个数一致
int[] nums = new int[3]{1, 2, 3};
// 在设定好数值时, 可以隐去设定长度
int[] nums = new int[]{1, 2, 3};

静态初始化

int[] nums = {1, 2, 3};

访问数组的长度

// 数组的长度
nums.Length

对数组进行排序

// 升序排序
Array.Sort(nums);

4.2 二维数组

动态初始化

int[,] nums = new int[2, 3];

静态初始化

int[,] nums = {{1, 2, 3}, {4, 5, 6}};

对二维数组进行排序

// 根据0号元素大小排序
Array.Sort(nums, (x, y) => x[0] - y[0]);

五、自定义类型

5.1 枚举类型

// 枚举类型的定义
enum Fruit {Apple, Banana, Orange
}// 枚举类型的实例化
Fruit myFruit = Fruit.Apple;

其中Fruit定义好之后,会自动按照从上到下的顺序为其分配0 ~ n - 1的值。也就是说,此时定义好的myFruit的值为0。
还可以手动给各种枚举值进行赋值

// 可以采用整型和字符型进行赋值, 其他的还有类似于byte, sbyte, long, short等都是可以的
enum Fruit {Apple = 10, Banana = 'a', Orange = 1
}

对于枚举类型,是不可以隐式转换为整型的,所以需要强制类型转换

// 以下两种转换方式都可以
int num = (int)myFruit;
int num = Convert.ToInt32(myFruit);
// 将整型强制转换回枚举类型
myFruit = (Fruit)5;

5.2 结构体

结构体的创建与初始化

// 结构体的定义
struct Student {public string name;public int age;
}// 实例化一个结构体
Student xiaoMing;
Student xiaoMing = new Student();
// 初始化数据
xiaoMing.name = "小明";
xiaoMing.age = 18;

结构体的构造函数。可以给出默认值,若给出默认值,不放入参数初始化时将对其赋予默认值。

// 带有构造函数的结构体
struct Student {public string name;public int age;// 结构体的构造函数, 并且可以直接赋上默认值public Student(string name="", int age=0) {// 这里的this与C++的this指针相仿this.name = name;this.age = age;}
}// 此时就可以直接调用构造函数初始化
Student xiaoMing = new Student(name, age);

当然,结构体当中除了构造函数,还可以写其他的函数。

// 带有构造函数的结构体
struct Student {public string name;public int age;// 结构体的构造函数public Student(string name="", int age=0) {this.name = name;this.age = age;}// 打印学生所有信息public void PrintStudentInfo() {Console.WriteLine(name);Console.WriteLine(age);}
}// 打印小明的信息
Student xiaoMing = new Student("小明", 18);
xiaoMing.PrintStudentInfo();

附:结构体不能用作链表、二叉树等节点的定义,这些应该用类去定义。

六、访问修饰符

6.1 访问修饰符简介

public:同一程序集中的任何其他代码或引用该程序集的其他程序集都可以访问该类型或成员。
protected:同一类或结构此类的派生类中的代码才可以访问的类型或成员。
private:同一类或结构中的代码可以访问该类型或成员。
internal:同一程序集中的任何代码都可以访问该类型或成员,但其他程序集中的代码不可以。
protected internal:同一程序集中的任何代码或其他程序集中的任何派生类都可以访问该类型或成员。

6.2 默认访问修饰符

类:默认修饰符为internal。
接口:默认的修饰符为internal。
结构体:默认的修饰符为internal。
枚举:默认的修饰符为internal。
委托:默认的修饰符为interna。

枚举:默认的修饰符为public,且不可修改。
结构体:默认的修饰符为private,不可使用protected。
类成员:默认修饰符为private。
接口成员:默认的修饰符为public,且不可修改。
委托:默认的修饰符为private。

七、函数

7.1 函数的基础运用

最简单的函数使用

/* 基本的函数语法
修饰符 返回值类型 函数名 (参数1, 参数2, ...) {// 函数体
}
*/
// 打印str
public static void Print(string str) {Console.WriteLine(str);
}

7.2 ref的使用

ref的作用是将值类型的参数转化引用类型来用。加上ref的前缀以后,原本传入的值将变为该值的地址。类似于C++的&

// 交换两数, 其中参数类型前加上ref就可以强制变为引用类型
public static void Swap(ref int num1, ref int num2) {int temp = num2;num2 = num1;num1 = temp;
}int num1 = 1;
int num2 = 2;
// 调用函数时也要加上ref
Swap(ref num1, ref num2);

注:使用ref的变量必须赋予初值,否则在未分配内存地址的状态下是无法使用ref的。

7.3 out的使用

out与ref的使用时的写法一致,都需要在函数的定义和调用中写明。out的作用是将参数当做一个输出通道,可以将结果放入该变量。

// 两数之和
public static Add(int num1, int num2, out int ans) {ans = num1 + num2;
}int num1 = 5;
int num2 = 10;
int ans;
// 调用函数
Add(num1, num2, out ans);

注:使用out的变量必须在函数中赋值

7.4 函数的重载

参数不同的同名函数定义便是重载

public int Add(int num1, int num2) {return num1 + num2;
}
public float Add(float num1, float num2) {return num1 + num2;
}

7.5 递归函数

递归函数就是自己调用自己。计算阶乘就是一个简单的应用。

// 计算阶乘
public static int Factorail(int num) {if (num == 0) {return 1;}else {return Factorail(num - 1) * num;}
}

还可以实现类似二叉树遍历的算法。

// 中序遍历
public void InOrder(TreeNode root, IList<int> ans) {// 该节点遍历结束if (root == null) {return;}// 左根右顺序遍历InOrder(root.left, ans);ans.Add(root.val);InOrder(root.right, ans);
}// 记录中序遍历结果
public IList<int> InorderTraversal(TreeNode root) {// 记录结果IList<int> ans = new List<int>();InOrder(root, ans);return ans;
}

八、类和对象

8.1 类的基础运用

类是一系列拥有相同属性方法的实例的抽象,每个对象都是类的一个实例,一个类可以有很多个对象。

// 类的创建
public class Person {// 成员变量public string name;public int age;// 构造函数public Person(string name="", int age=0) {this.name = name;this.age = age;}// 类内方法public void Eat(string food) {Console.WriteLine($"{name}吃了一个{food}");}
}// 实例化一个对象
Person xiaoMing = new Person("小明", 18);
// 调用方法
xiaoMing.Eat("苹果");

类内为了方便读写私有成员变量,设计了属性机制,类似于Python的property()。

class Person {// 成员变量private string name;// name的属性public string Name {get { return name; }set { name = value; }}// 构造函数public Person(string name) {this.name = name;}
}Person xiaoMing = new Person("小明");
// 读取名字
string name = xiaoMing.Name;
// 写入名字
xiaoMing.Name = "小茗";

还有一些简写属性的写法

// 可读可写的两种简写写法
public string Name { get; set; }
public string Name {get => name;set => name = value;
}
// 可读不可写的三种简写写法
private string name;
public string Name => name;
public string Name { get; }
public string Name { get; private set; }

自动实现的属性,像{ get; }这种写法在类型是string时会出现警告CS8618,有时无法正常工作。所以简写时需要注意场合。


附:二叉树节点的定义

// 以下两种定义方式是等价的
public class TreeNode {public int val;public TreeNode? left;public TreeNode? right;public TreeNode(int val=0, TreeNode? left=null, TreeNode? right=null) {this.val = val;this.left = left;this.right = right;}
}class TreeNode(int val=0, TreeNode? left=null, TreeNode? right=null) {public int val = val;public TreeNode? left = left;public TreeNode? right = right;
}

类弥补了结构体不能引用自身的缺陷,所以可以使用类来定义类似的节点。

8.2 继承

基础的继承语法

// 这里表示学生类继承人类
class Person {}
class Student: Person {}

继承自父类的子类可以继承其访问修饰符为public和protected的字段、属性、方法等。
另外,如果父类的构造函数只有有参数的构造函数,而没有重载无参构造函数时,那么在子类写构造函数的时候应当用base()。这时才能在实例化子类时正常构造父类,否则父类在构造时无法调用有参的构造函数,而由于没有无参构造函数,也没有可以调用的无参构造函数,所以就无法调用构造函数。因为在实例化子类时,一定是先构造父类再构造子类的,所以无法调用父类构造函数,自然就表示无法实例化成功子类。
这里的name是string类型的,要保证在退出构造函数时,字段name必须包含非null值(CS8618警告),所以这个例子当中必须有参,至少也要对name进行初始化。

class Person {public string name;protected char sex;private int age;// 只有有参构造函数public Person(string name, char sex, int age) {this.name = name;this.sex = sex;this.age = age;}public int Age {get => age;set => age = value;}// 打印个人信息protected void PrintPerInfo() {Console.WriteLine(name);Console.WriteLine(sex);Console.WriteLine(age);}
}class Student: Person {public string school;// 这时需要用base函数给父类构造函数传参, 以此来调用父类构造函数public Student(string name, char sex, int age, string school): base(name, sex, age) {this.school = school;}// 打印学生信息public void PrintStuInfo() {// 可以调用父类的函数PrintPerInfo();Console.WriteLine(school);}// 修改信息public void UpdateInfo(string name, char sex, int age, string school) {// public和protected直接调用没问题this.name = name;this.sex = sex;// private字段不能直接调用Age = age;this.school = school;}
}

C#不支持多重继承,不过可以用接口来实现多重继承。

8.3 多态

多态分为静态多态性和动态多态性。

8.3.1 静态多态性

C#提供了函数重载和运算符重载来实现静态多态性。
1)函数重载见7.4 函数的重载
2)运算符重载的实现方法:

class Line {public double length;public Line(double length) {this.length = length;}// 实现了两个线段相加, 此时就可以直接用Line + Line的操作了public static Line operator+ (Line l1, Line l2) {return new Line(l1.length + l2.length);}
}
8.3.2 动态多态性

动态多态性是用抽象类、抽象方法和虚方法来实现的,即abstract和virtual。
1)抽象类和抽象方法实现的动态多态性。

// 抽象类不可以被实例化, 且不可以与sealed关键字同时使用
abstract class Shape {// 抽象方法一定要在抽象类中, 且抽象方法是不允许声明主体的abstract public double Area();
}// 正方形类
class Square: Shape {public double width;// 类内一定要实现继承的抽象类当中的抽象方法, 实现的方法前面要加上overridepublic override double Area() {return width * width;}
}// 长方形类
class Rectangle: Shape {public double width;public double height;// 类似的长方形面积计算实现public override double Area() {return width * height;}
}

2)虚方法实现的动态多态性

// 抽象类当中是可以放除了抽象方法之外的方法的
abstract class Shape {// 相比抽象方法, 虚方法是一定要声明主体的public virtual void Draw() {Console.WriteLine("开始绘画图形……");}
}class Square: Shape {public override void Draw() {// 先执行Shape的绘画函数(这里语法上不强制执行父类的函数)base.Draw();// 再执行自己的语句Console.WriteLine("绘制正方形成功");}
}class SquareToRectangle: Square {public override void Draw() {// 这里执行时会执行Square的绘画函数base.Draw();// 由于Square的绘画函数会调用Shape的绘画函数, 所以这里是三句话都打印Console.WriteLine("将正方形修改为长方形成功");}
}

附:static和sealed的使用方法
static修饰符可用于类、字段、方法、属性、运算符、事件和构造函数,但不能用于索引器、析构函数或类以外的类型。

// 静态类无法被实例化, 一般用来写工具类
static class 类名 {// 静态成员(静态类不能有非静态的成员)public static 变量类型 变量名;// 静态函数的声明方法public static 返回类型 函数名() {// 函数体}
}
// 调用方法
类名.变量名;
类名.函数名();

sealed的使用方法:

// 此时表示该类为密封类, 任何其他类都不得继承它
sealed class 类名 {}
// 此时表示该函数为密封函数, 继承自该函数所在类的子类都无法重写该函数
访问修饰符 sealed override 返回类型 函数名() {}

九、接口

接口的定义与类类似,不过能存放的只有属性、方法、委托。

interface IUSB {public int USBType { get; set; }// 接口内是不能实现方法的public void WriteFile(string content);
}// Computer实现接口ISUB
class Computer: IUSB {// 一般类内一定要实现所有接口内的成员public int USBType { get; set; }public void WriteFile(string content) {Console.WriteLine("写入文件成功");}
}// 抽象类内的抽象方法便不用实现
abstract class Computer: IUSB {public int USBType { get; set; }abstract public void WriteFile(string content);
}interface IVGA {}// 接口可以实现多个其他接口, 但内部仍然无法声明, 只能定义
interface ITypeC: IUSB, IVGA {}class Machine {}// 假如一个类要继承另一个类和若干个接口, 那么应该先写类
class Computer: Machine, IUSB, IVGA {}

十、泛型

泛型类似于C++的模板类,可以不用指定特定的类型,只给出一个抽象的类型,在调用的时候再指定类型去调用。

// 其中T便是给出的抽象类型
public static void Print<T>(T content) {Console.WriteLine(content);
}// 调用
Print<int>(1);
// 还可以简写
Print(1);// 若需要不止一个抽象类型时, 就用逗号隔开各抽象类型即可
public static void PrintTwoInfo<T, V>(T content1, V content2) {Console.WriteLine(content1);Console.WriteLine(content2);
}// 调用
PrintTwoInfo<string, int>("abc", 123);
// 也可以简写
PrintTwoInfo("abc", 123);

泛型还可以使用在类和接口上,使用方法都是在类名或接口名后面加上<类型名, …>。

class Test<T> {}
interface ITest<T> {}// 在继承父类或实现接口时, 就需要对这些泛型进行确定, 即使确定的类型还是泛型也可以
class Test2: Test<int> {}
interface ITest2<T>: ITest<T> {}

泛型还可以被约束,采用where关键字来约束。
约束的类型有以下几种:
1)new()约束:表示不能为抽象类型。
2)基类/接口约束:表示<>里面必须是基类/接口本身或者是派生自基类的其他类/接口。
3)引用/值类型约束:用class/struct来约束只能为引用/值类型。
4)组合约束:第一项必须是引用/值类型约束或基类约束,接下来是接口约束,最后是new()约束。
class、struct、unmanaged、notnull、default约束不能组合或重复,并且必须先在约束列表中进行指定。

// 语法: 名称<泛型>(参数) where 泛型1: 约束1, 约束2, ... where 泛型2: 约束, ...
public static Print<T, V>(T content1, V content2) where T: new()where V: class {// 类的主体部分
}

注:构造函数不用加<泛型>

十一、委托和事件

委托是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用。 在实例化委托时,你可以将其实例与任何具有兼容签名和返回类型的方法相关联。 你可以通过委托实例调用方法。

public static void BuyApple() {System.Console.WriteLine("买了一个苹果");
}public static void BuyBanana() {System.Console.WriteLine("买了一个香蕉");
}// 定义一个委托
public delegate void BuyFruitEventHandle();
// 实例化一个委托
BuyFruitEventHandle buyApple = new BuyFruitEventHandle(BuyApple);
// 调用
buyApple();// 委托还可以多播
BuyFruitEventHandle buyFruit = new BuyFruitEventHandle(BuyApple);
buyFruit += BuyBanana;
// 这时调用就会先后调用BuyApple和BuyBanana
buyFruit();

事件就是一个特殊的委托对象,它在类内可以=、+=、-=和调用,但是在类外只能+=和-=。且事件的定义应该在函数的外面。

class BuyFruit {public delegate void BuyFruitEventHandle();public event BuyFruitEventHandle buyFruitEvent = BuyApple;public static void BuyApple() {System.Console.WriteLine("买了一个苹果");}public void TestEvent() {// 类内四种操作都可以做buyFruitEvent = BuyApple;buyFruitEvent += BuyApple;buyFruitEvent -= BuyApple;buyFruitEvent();}
}class Test {static void Main(string[] args) {BuyFruit bf = new BuyFruit();// 类外就只能做这两种操作person.buyFruitEvent += Person.BuyApple;person.buyFruitEvent -= Person.BuyApple;// 想要调用只能通过类内的其他方法间接调用bf.TestEvent();}
}

十二、异常处理

使用throw抛出异常

// 提醒内容为content的异常抛出
throw new SystemException(string content);

try catch来捕捉异常

int[] a = new int[2];
// 尝试执行一段代码
try {// 代码段
}
// 捕获括号内的异常, 捕获成功后执行catch中的代码
// catch不写拦截的异常类型,则拦截所有异常
// 可以写多个catch, 子类写在父类前面
catch (System.Exception e) {// 代码段
}
// 无论是否捕获到异常, 都会执行finally中的代码
finally {// 代码段
}

十三、反射

获取类型

// 使用typeof
System.Type intType = typeof(int);
// 使用GetType
int num;
System.Type intType = num.GetType();
// 通过静态方法获取
System.Type intType = System.Type.GetType("System.Int32");// 类型包含的属性有Name, FullName, Namespace, IsAbstract, IsArray, IsClass, IsEnum, IsInterface, IsPublic, IsSealed, IsValueType等
string name = intType.Name;// 类型包含的方法有GetConstructor(), GetEvent(), GetField(), GetInterface(), GetMember(), GetMethod(), GetProperty(), 方法后面加s返回所有的内容

实例化私有类

// 实例化无参类
Type privateClassType = typeof(PrivateClass);
object instance = Activator.CreateInstance(privateClassType, true);
// 实例化有参类
object Activator.CreateInstance(Type type, BindingFlags bindingAttr, Binder binder, object[] args, CultureInfo culture);

获取字段信息

Type t = typeof(T);
FieldInfo nameField = t.GetField("name");
// obj为私有类对象
// 设置私有类对象的name属性
nameField.SetValue(obj, "newName");
// 获取私有类对象的name属性
object name = nameField.GetValue(obj);

获取方法信息

// 获取方法
MethodInfo Type.GetMethod(string name, BindingFlags bindingAttr, Binder binder, Type[] types, ParameterModifier[] modifiers);
// 调用方法
object MethodBase.Invoke(object obj, object[] parameters);

十四、特性

特性是对各种元素的进一步描述。在unity开发过程中,经常会用到特性。

// 标题提示
[Header("xxx")]
// 范围
[Range(1, 100)]
// 可序列化
[System.Serializable]
// 没有宏定义参数, 则忽略方法或属性等
[Conditional("xxx")]
// 标记过时的方法, 参数为true时调用会报错
[ObsoleteAttribute(string message, bool error)]
// 在添加组件列表当中添加一个自定义的组件
[AddComponentMenu("xxx")]

读取特性的方法

// 使用反射来读取特性
Type type = typeof();
object[] atts = type.GetCustomAttributes(bool inherit);

创建自定义特性的方法

// 第一个参数表示应用范围, 第二个参数表示是否为派生类和重写成员继承, 第三个参数表示是否可以使用多个该特性给一个元素
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
// 必须继承Attribute
class MyAttribute : Attribute {public MyAttribute() {}
}

常用数据结构

一、动态数组(ArrayList和List)

类似于Python的list,可以存放任意类型,可以自由添加删除元素,但效率较低。

// 定义一个动态数组
ArrayList list = new ArrayList();
// 数组元素个数
int n = list.Count;
// 添加一个元素, 返回元素添加的序列
int list.Add(object? value);
// 删除所有元素
void list.Clear();
// 判断元素是否在动态数组中
bool list.Contains(object? item);
// 在index位置插入一个元素
void list.Insert(int index, object? value);
// 删除第一个匹配项
void list.Remove(object? obj);
// 删除指定序列的元素
void list.RemoveAt(int index);
// 删除从index开始的count个元素
void list.RemoveRange(int index, int count);
// 反转列表元素
void list.Reverse();
// 反转从index开始的count个元素
void list.Reverse(int index, int count);
// 对内部元素进行排序
void list.Sort();

动态数组还有类似的List,它的用法除了固定了元素的类型,其余基本与ArrayList一致,就不展开说明了。

二、栈(Stack)和队列(Queue)

栈的使用

// 实例化一个栈
Stack stack = new Stack();
// 栈元素个数
int n = stack.Count;
// 删除栈内所有元素
void stack.Clear();
// 查找元素obj是否存在于栈内
bool stack.Contains(object? obj);
// 返回栈顶元素, 但不会弹出该元素
object? stack.Peek();
// 将栈顶元素弹出并返回
object? stack.Pop();
// 将一个元素压入栈
void stack.Push(object? obj);

队列的方法与栈基本一致,只是入队和出队有些差别。

// 实例化一个队列
Queue que = new Queue();
// 队列元素个数
int n = que.Count;
// 将一个元素放到队尾
void que.Enqueue(object? obj);
// 弹出并返回队首元素
object? que.Dequeue();

三、集合(HashSet和SortedSet)

集合的基本操作依旧是那几个方法,只是多了一些集合特有的方法,例如并、交。

// 实例化一个集合
HashSet<int> set = new HashSet<int>();
// 判断other是否为set的完全子集
bool set.IsProperSubsetOf(IEnumerable<int> other);
// set ∪ other
void set.UnionWith(IEnumerable<int> other);
// set ∩ other
void set.IntersectWith(IEnumerable<int> other);
// set - other
void set.ExceptWith(IEnumerable<int> other);

SortedSet和HashSet相比,就是多了一个元素自动排序的功能,放入的元素将按由小到大的顺序排列。

四、哈希表(Hashtable)和字典(Dictionary)

哈希表和字典都是键值对应的一种数据结构,只不过字典必须指定类型,哈希表不需要指定类型。也因此,哈希表需要装箱拆箱的操作,而字典则不需要。所以论效率是字典的速度更快,而想要什么类型都能放就需要用哈希表了。

// 实例化一个哈希表
Hashtable map = new Hashtable();
// 哈希表键值对个数
int n = map.Count;
// 哈希表所有的键
map.Keys;
// 哈希表所有值
map.Values;
// 添加一个键值对
void map.Add(object key, object? value);
// 判断是否存在key为键的键值对
bool map.Contains(object key);
bool map.ContainsKey(object key);
// 判断是否存在value为值的键值对
bool map.ContainsValue(object? value);
// 删除以key为键的键值对
void map.Remove(object key);

字典与哈希表常用的方法类似,只是字典在访问不存在的键时会报错,这点需要注意。

// 实例化一个字典
Dictionary<int, int> dict = new Dictionary<int, int>();
// 尝试获取key所对应的值, 并将获取的值给value, 返回是否成功获取值
bool dict.TryGetValue(int key, out int value);

五、堆/优先队列(PriorityQueue)

堆又称优先队列,分为小根堆和大根堆。在C#中,PriorityQueue默认为小根堆。
PriorityQueue有两个泛型,第一个TElement是堆存放的元素,并不是堆化时优先级判断的标准;TPriority是用来判断堆化的优先级的,由于是小根堆,所以越小越靠近根。

// 实例化一个堆
PriorityQueue<int, int> heap = new PriorityQueue<int, int>();
// 将element放入堆
void heap.Enqueue(int element, int priority);
// 弹出头一个元素
int heap.Dequeue();

六、双向链表(LinkedList)

双向链表可以用来模拟双端队列。

LinkedList<T> linkedList = new LinkedList<T>();
# 添加
linkedList<T>.AddFirst(LinkedListNode<T> node);
linkedList<T>.AddFirst(T value);
linkedList<T>.AddLast(LinkedListNode<T> node);
linkedList<T>.AddLast(T value);
# 查看
linkedList.First.Value
linkedList.Last.Value
# 删除
linkedList.RemoveFirst();
linkedList.RemoveLast();

System命名空间

包含定义常用值和引用数据类型、事件和事件处理程序、接口、属性以及处理异常的基本类和基类。

一、Console类

表示控制台应用程序的标准输入流、输出流和错误流。 此类不能被继承。

1.1 输出语句

// 末尾不会输出\n, 类似于Python的print(end="")
Console.Write();
// 末尾默认输出\n, 类似于Python的print()
Console.WriteLine();

输出字符串需要拼接时可以采用+或占位符进行输出。

// 下面两种输出方式等价
Console.WriteLine("aaa" + "bbb");
Console.WriteLine("{0}{1}", "aaa", "bbb");

其中占位符的0和1可以互换。

Console.WriteLine("{1}{0}", "bbb", "aaa");

还有一种更好用的方法,就是在字符串的前面加上$符号,类似于Python的f"xxx"。
并且这种方法不仅仅可以用于输出,由于它本身就是一个字符串,所以也可以参与运算。

int num = 5;
// 输出时使用
Console.WriteLine($"这里有{num}片叶子");
// 赋值时使用
string str = $"这里有{num}片叶子";

1.2 输入语句

// 读取一个键
Console.ReadKey();
// 读取一行输入
Console.ReadLine();

我运行这两句话时候没问题,但是不能让其返回值赋值到变量上,有一个警告说返回可能为null和一个报错说无法转换类型,不过我基本用不到这个语句,就没再理会了。

二、Convert类

将一个基本数据类型转换为另一个基本数据类型。

// 比较典型的类型转换函数, 其他还有很多
Convert.ToBoolean();
Convert.ToChar();
Convert.ToDouble();
Convert.ToInt32();
Convert.ToSingle();

除了这种转换方式,还可以用数据类型自带的方法Parse()来转换。

bool num = bool.Parse(val);
char num = char.Parse(val);
int num = int.Parse(val);
float num = float.Parse(val);
double num = double.Parse(val);

将其他类型变为字符串可以用数据类型自带的方法ToString()来转换。

string str = val.ToString();

三、Random类

表示伪随机数生成器,这是一种能够产生满足某些随机性统计要求的数字序列的算法。

// 首先要实例化
Random rand = new Random();
// 制定一个种子进行初始化
Random rand = new Random(int seed);
// 随机返回一个32位非负整数
int rand.Next();
// 在0 ~ maxValue的左闭右开的区间随机返回一个整数, 如果maxValue == 0, 那么返回0
int rand.Next(int maxValue);
// 在minValue ~ maxValue的左闭右开的区间随机返回一个整数, 如果minValue == maxValue, 那么返回minValue
int rand.Next(int minValue, int maxValue);
// 随机返回一个64位非负整数
long rand.NextInt64();
// 在0 ~ maxValue的左闭右开的区间随机返回一个整数, 如果maxValue == 0, 那么返回0
long rand.NextInt64(long maxValue);
// 在minValue ~ maxValue的左闭右开的区间随机返回一个整数, 如果minValue == maxValue, 那么返回minValue
long rand.NextInt64(long minValue, long maxValue);

四、String类

将文本表示为 UTF-16 代码单元的序列。

// 判断strA与strB的字典序, strA在前返回-1, 一样返回0, strA在后返回1
int String.Compare(string strA, string strB);
// 类似的, 与Compare一致
int strA.CompareTo(string strB);
// 判断strA中是否包含strB
bool strA.Contains(string strB);
// 返回strA中第一次出现strB的位置, 若从未出现过, 则返回-1
int strA.IndexOf(string strB);
// 返回strA中从startIndex位置开始第一次出现strB的位置, 若从未出现过, 则返回-1
int strA.IndexOf(string strB, int startIndex);
// 将strA在startIndex处插入一个strB, 并返回结果
string strA.Insert(int startIndex, string strB);
// 返回序列为0 ~ startIndex - 1的字符串
string str.Remove(int startIndex);
// 将str当中的oldValue替换为newValue, 并返回结果
string str.Replace(string oldValue, string newValue);
// 将strA在strB处切开, 并返回切分后的结果
string[] strA.Split(string strB);
// 从strA中在startIndex开始的位置取出length长度的字符串并返回
string strA.Substring(int startIndex, int length);
// 去除str首尾的空白, 包括空格, \n, \t等, 并返回结果
string str.Trim();
// 去除str尾部的空白, 包括空格, \n, \t等, 并返回结果
string str.TrimEnd();
// 去除str首部的空白, 包括空格, \n, \t等, 并返回结果
string str.TrimStart();

System.IO命名空间

包含允许读取和写入文件和数据流的类型,以及提供基本文件和目录支持的类型。

一、File类

提供用于创建、复制、删除、移动和打开单一文件的静态方法,并协助创建 FileStream 对象。

// 将content写入path指向的文件
File.WriteAllText(string path, string content);

System.Text命名空间

包含表示 ASCII 和 Unicode 字符编码的类;用于字符块和字节块相互转换的抽象基类;以及无需创建 String 的中间实例即可操作 String 对象并设置其格式的帮助程序类。

一、StringBuilder类

表示可变字符字符串。 此类不能被继承。在需要经常修改字符串时常用。

using System.Text
// 创建字符串
StringBuilder str = new StringBuilder(string value);
// 追加字符串strB
StringBuilder strA.Append(string strB);
// Format形式追加函数和示例
StringBuilder str.AppendFormat(string format, params object[] args);
str.AppendFormat("吃{0}喝{1}", "饭", "水");
// 将str在index处插入一个value
StringBuilder str.Insert(int index, string value);
// 删除str从序列startIndex开始长度为length的子串
StringBuilder str.Remove(int startIndex, int length);
// 将str当中的oldValue替换为newValue
StringBuilder str.Replace(string oldValue, string newValue);

UnityEngine命名空间

一、MonoBehaviour类

继承的变量:

变量作用
enabled当前对象的可用性
isActiveAndEnabled报告游戏对象及其关联的行为是否处于活动状态并启用
gameObject当前游戏对象
tag对象的标签
transform挂载到该对象的Transform组件
hideFlags该对象应该隐藏、随场景一起保存还是由用户修改
name对象的名称

常见的生命周期方法包括以下几种:
Awake:当脚本实例被加载时调用。这个方法在所有的Start方法之前调用,即使该脚本的对象未被启用也会调用。
OnEnable:当对象被启用时调用,这发生在Awake之后,Start之前。
Start:当游戏开始后,对象被启动时调用,但是整个游戏过程只调用一次。
FixedUpdate:在每个物理步骤中调用,与帧率无关,适合进行物理相关的计算,默认间隔时长0.02s。
Update:每帧调用一次,适合进行常规的游戏逻辑更新。
LateUpdate:在所有Update函数调用后调用,适合进行依赖于Update的操作,例如摄像机追踪。
OnGUI:OnGUI在GUI渲染时调用,大概是每帧两次。
OnDisable:当对象被禁用或者脚本被销毁时调用。
OnDestroy:当脚本或者游戏对象被销毁时调用。

其他的方法:

// 和Debug.Log()一个效果
MonoBehaviour.print();

二、GameObject类

// 寻找其他对象, 返回对应名字的对象
GameObject.Find(string name);
// 寻找对应标签的对象
GameObject.FindWithTag(string tag);
GameObject.FindGameObjectWithTag(string tag);
// 寻找所有对应标签的对象
GameObject.FindGameObjectsWithTag(string tag);
// 游戏对象的激活状态, 只读
gameObject.activeSelf
// 设置对象的激活状态
gameObject.SetActive(bool value);
// 添加组件
gameObject.AddComponent<T>();
// 获取组件
gameObject.GetComponent<T>();

三、Vector3结构体

Vector3 vector = new Vector3(float x, float y, float z);
// 标准化
Vector3.Normalize();
vector.normalized
// 向量的模
vector.magnitude
// 模的平方
vector.sqrMagnitude
// (1, 0, 0)
Vector3.right
// (0, 1, 0)
Vector3.up
// (0, 0, 1)
Vector3.forward
// (0, 0, 0)
Vector3.zero
// (1, 1, 1)
Vector3.one
// 两点距离
Vector3.Distance(Vector3 a, Vector3 b);
// 两个向量的夹角
Vector3.Angle(Vector3 a, Vector3 b);
// 两个向量的叉乘
Vector3.Cross(Vector3 a, Vector3 b);
// 两个向量的点乘
Vector3.Dot(Vector3 a, Vector3 b);
// 插值函数, 在两个点之间差值, 位置是a + (b - a) * (1 + t)
Vector3.Lerp(Vector3 a, Vector3 b, float t);

四、Quaternion结构体

// 空旋转
Quaternion.identity
// z轴朝向forward所指方向
Quaternion.LookRotation(Vector3 forward);
// 插值方法
Quaternion.Lerp(Quaternion a, Quaternion b, float t);

五、Time类

// 上帧结束到这帧开始的时间间隔
Time.deltaTime
// FixedUpdate两帧间隔
Time.fixedDeltaTime
// 游戏开始到现在的时间间隔
Time.time
// 时间快慢比例
Time.timeScale

六、Mathf结构体

包含各种数学操作

七、Input类

// 按住key
Input.GetKey(KeyCode key);
// 按下key
Input.GetKeyDown(KeyCode key);
// 松开key
Input.GetKeyUp(KeyCode key);// 0->左, 1->右, 2->中
// 按住鼠标按键
Input.GetMouseButton(int button);
// 按下鼠标按键
Input.GetMouseButtonDown(int button);
// 松开鼠标按键
Input.GetMouseButtonUp(int button);// 虚拟轴, 八方向, 世界坐标移动
// Horizontal横向, -1左, 1右, 0中
// Vertical纵向, -1下, 1上, 0中
// Mouse X和Mouse Y
Input.GetAxis(string axisName);
// 虚拟按键
// 按住按键
Input.GetButton(string buttonName);
// 按下按键
Input.GetButtonDown(string buttonName);
// 松开按键
Input.GetButtonUp(string buttonName);

八、Object类

// 生成一个实例
Instantiate(GameObject original, Vector3 position, Quaternion rotation);
// 在t秒后销毁obj
Destroy(Object obj, float t);

九、Ray结构体

Ray ray = new Ray(Vector3 origin, Vector3 direction);
// 检测射线方向的物体
bool Physics.Raycast(Ray ray, out RaycastHit hitInfo);
// 检测射线方向所有物体
RaycastHit[] Physics.RaycastAll(Vector3 origin, Vector3 direction, float maxDistance, int layerMask, QueryTriggerInteraction queryTriggerInteraction);

十、GUI/GUILayout类

GUILayout与GUI的区别在于前者会自动布局。

// 标签
void GUI.Label(Rect position, string text);
// 按钮, 返回是否按下
bool GUI.Button(Rect position, string text);
// 宽输入框
string GUI.TextArea(Rect position, string text);
// 单行输入框
string GUI.TextField(Rect position, string text);

标签属性(Attribute)

序列化,可以转换为二进制流

[System.Serializable]
public class Data {public int num;
}

标题,会显示在Inspector面板中

[UnityEngine.Header("年龄")]
public int age;

范围限制

[UnityEngine.Range(0, 100)]
public int num;

添加Inspector面板中与上一个字段的间距

[UnityEngine.Space(10)]
public int num

鼠标悬停注释

[UnityEngine.Tooltip("输入年龄")]
public int age;

隐藏字段,将不能在Inspector面板中查看

[UnityEngine.HideInInspector]
public int num;

新建一个可创建的项目

// 新建一个创建ScriptableObject的选项
[UnityEngine.CreateAssetMenuAttribute(fileName = "NewData", menuName = "ScriptableObjects/MyScriptableObject", order = 1)]
public class MyScriptableObject : ScriptableObject {public string objectName;
}

新建一个菜单栏选项

[MenuItem("Tools/MyTool")]

Unity组件

一、Transform组件

// 世界坐标
transform.position
// 相对父级的坐标
transform.localPosition
// 世界旋转
transform.eulerAngles
transform.rotation
// 相对父级的旋转
transform.loaclEulerAngles
transform.localRotarion
// 相对父级缩放
transform.localScale// 移动方法
transform.Translate(Vector3 translation);
transform.position += new Vector(0f, 0f, 1f);
// 旋转方法
transform.Rotate(Vector3 eulers);
transform.RotateAround(Vector point, Vector3 axis, float angle);
// 三个方向向量(朝向物体自身的前、上、右)
transform.forward
transform.up
transform.right// 父级transform
transform.parent
// 根对象transform
transform.root
// 子对象个数
transform.chilfCount
// 获取子物体transform
transform.GetChild(int index);
// 设置父物体transform
transform.SetParent(Transform p);
// 查找子物体, 用/来标记子物体层级
// 例: transform.Find("A/B/C");
transform.Find(string n);

二、Rigidbody组件

// 添加力
Rigidbody.AddForce(Vector3 force);
// 添加爆炸力
Rigidbody.AddExplosionForce(float explosionForce, Vector3 explosionPosition, float explosionRadius, float upwardsModifier, ForceMode mode)

三、Collider组件

检测碰撞的必要条件:两个物体都有Collider组件且至少有一个物体有RigidBody组件。

// 三个碰撞回调函数
void OnCollisionEnter(Collision other) {}
void OnCollisionStay(Collision other) {}
void OnCollisionExit(Collision other) {}
// 三个交叉(触发)回调函数
void OnTriggerEnter(Collider other) {}
void OnTriggerStay(Collider other) {}
void OnTriggerExit(Collider other) {}
// 碰撞点
ContactPoint[] Collision.contacts { get; }

注:如果使用的是Collider2D和RigidBody2D,那么碰撞函数也要加2D。例:OnCollisionEnter2D

四、Shadow/Outline组件

文字特效组件


Shader

可以控制材质球的渲染方式。

一、基础结构和属性

1.1 基础结构

// Shader的目录结构
Shader "Custom/NewSurfaceShader" {// Shader外部属性Properties {}// 可以写多个SubShaderSubShader {// 显卡A使用的子着色器}SubShader {// 显卡B使用的子着色器}// 如果没有匹配的SubShader, 就会使用FallBackFallBack "Diffuse"
}

1.2 各种属性类型

Shader "Custom/NewSurfaceShader" {// Shader属性Properties {// 语法: 属性名(显示名, 类型) = 默认值// 浮点类型_FloatValue("Float Value", Float) = 1.0// 范围浮点类型_RangeValue("Range Value", Range(0, 1)) = 0.5// 四维向量类型_VectorValue("Vector Value", Vector) = (1, 1, 1, 1)// 颜色类型_ColorValue("Color Value", Color) = (1, 1, 1, 1)// 二阶贴图类型_MainTex("Main Texture", 2D) = "" {}// 三阶贴图类型_3D("3D", 3D) = "black" {}// 非二阶贴图类型_RectValue("Rect Value", Rect) = "" {}// 立方体贴图类型_CubeValue("Cube Value", Cube) = "" {}}
}

ShaderLab和CG变量类型匹配关系
在这里插入图片描述

1.3 语义

应用阶段传递模型数据给顶点着色器时支持的语义
在这里插入图片描述
从顶点着色器传数据给片元着色器支持的语义
在这里插入图片描述
片元着色器输出时支持的语义
在这里插入图片描述

1.4 SubShader简介

SubShader基本结构

SubShader {// 可选的[Tags]// 可选的[RenderSetup]Pass {}// 其他通道
}

SubShder标签类型
SubShder标签类型

1.5 常见渲染状态设置

常见渲染状态设置

1.6 Unity内置变换矩阵

内置变换矩阵

二、固定管线着色器

2.1 简单说明

固定管线着色器已经逐渐弃用了,尽量不要用该着色器了。

2.2 基础语法

固定颜色

SubShader {Pass {// 固定颜色Color(1, 0, 0, 1)}
}

固定可变颜色

Properties {// 颜色类型_ColorValue("Color Value", Color) = (1, 1, 1, 1)
}// 可以写多个SubShader
SubShader {Pass {// 固定颜色Color[_ColorValue]}
}

Material命令

Properties {_DiffuseColor("Color Value", Color) = (1, 1, 1, 1)_AmbientColor("Ambient Color Value", Color) = (1, 1, 1, 1)_SpecularColor("Specular Color Value", Color) = (1, 1, 1, 1)_EmissionColor("Emission Color Value", Color) = (1, 1, 1, 1)_Shininess("Shininess", Range(0.01, 1)) = 0.5
}SubShader {Pass {// 开启光照, 可用On, OffLighting On// 开启镜面反射, 可用On, OffSeparateSpecular On// 剔除, 可用Back, Front, OffCull Back// 可用的混合// One: 使用此值使源或目标的颜色全部通过// Zero: 使用此值删除源或目标的颜色// SrcColor: 此阶段的值乘源颜色值// SrcAlpha: 此阶段的值乘源颜色的Alpha值// DstColor: 此阶段的值乘帧缓冲区源颜色值// DstAlpha: 此阶段的值乘帧缓冲区源颜色的Alpha值// OneMinusSrcColor: 此阶段的值乘 1 - SrcColor// OneMinusSrcAlpha: 此阶段的值乘 1 - SrcAlpha// OneMinusDstColor: 此阶段的值乘 1 - DstColor// OneMinusDstAlpha: 此阶段的值乘 1 - DstAlpha// 半透明混合Blend SrcAlpha OneMinusSrcAlpha// premultiplied transparencyBlend One OneMinusSrcAlpha// AdditiveBlend One One// Soft AdditiveBlend OneMinusDstColor One// MultiplicativeBlend DstColor Zero// 2x MultiplicativeBlend DstColor SrcColorMaterial {// 漫反射Diffuse[_DiffuseColor]// 环境光Ambient[_AmbientColor]// 镜面反射Specular[_SpecularColor]// 光泽度Shininess[_Shininess]// 自发光Emission[_EmissionColor]}}
}

正面和背面都渲染

SubShader {Pass {// 固定颜色Color[_ColorValue]}
}

Material命令

Properties {_DiffuseColor("Color Value", Color) = (1, 1, 1, 1)_AmbientColor("Ambient Color Value", Color) = (1, 1, 1, 1)_SpecularColor("Specular Color Value", Color) = (1, 1, 1, 1)_EmissionColor("Emission Color Value", Color) = (1, 1, 1, 1)_Shininess("Shininess", Range(0.01, 1)) = 0.5
}SubShader {Pass {Cull Back}Pass {Cull Front}
}

深度测试

SubShader {Pass {// 小于等于缓存位深度, 不被挡住ZTest LEqual// 关闭深度缓存ZWrite OffColor(1, 0, 0, 1)}Pass {// 大于缓存深度, 被挡住ZTest Greater// 关闭深度缓存ZWrite OffColor(0, 1, 0, 1)}
}

渲染队列

SubShader {Tags {// Background: 1000, Geometry: 2000, AlphaTest: 2450, Transparent: 3000, Overlay: 4000// 数字越大, 渲染越靠后, 覆盖前面的渲染结果"Queue" = "Background"}
}

设置纹理图片

Properties {_MainTex ("Texture", 2D) = "" {}_MainColor ("Color", Color) = (1, 1, 1, 1)
}SubShader {Pass {Material {Diffuse [_MainColor]}// 设置纹理图片SetTexture [_MainTex] {// Texture表示纹理图片, Primary表示光照计算颜色或绑定顶点颜色Combine Texture + Primary}// 固定颜色混合SetTexture [_MainTex] {ConstantColor (1, 1, 1, 1)// Constant表示ConstantColor的颜色Combine Texture + Constant}// 上次结果混合SetTexture [_MainTex] {// Previous表示使用上一个SetTexture的结果Combine Texture + Previous}SetTexture [_MainTex] {// 根据Constant的Alpha值来混合两个纹理Combine Texture Lerp(Constant) Previous}}
}

透明度测试

// 与深度测试的用法类似
AlphaTest

三、表面着色器

表面着色器不需要Pass通道,需要用cg语言编写

SubShader {// cg程序开始CGPROGRAM// 函数定义// 语法: #pragma surface 函数名 光照模型 特殊设置#pragma surface surf Lambert alphastruct Input {// 纹理贴图坐标float2 uv_MainTex;// 视图方向float3 viewDir;// 世界坐标float3 worldPos;// 屏幕空间坐标float4 screenPos;// 顶点插值颜色, COLOR是语义绑定float4 color: COLOR;};void surf(Input IN, inout SurfaceOutput o) {// 颜色纹理o.Albedo = float3(1, 1, 0);// 法向量o.Normal = float3(0, 0, 1);// 自发光颜色o.Emission = float3(0, 0, 0);// 高光颜色o.Specular = float3(0, 0, 0);// 光泽度o.Gloss = 0;// 透明度o.Alpha = 1;}// 数据类型包括float, float2, float3, float4, float4x4, half, int, fixed, bool, string, sampler2D, samplerCUBEfloat num = 1;half2 vec = half2(1, 2);// cg程序结束ENDCG
}

要想在cg语言中使用Properties的属性,需要在两个地方都声明同名变量

Properties {_Color ("Color", Color) = (1,1,1,1)
}SubShader {//使用可以用_Color.rgba或_Color.xyzwfixed4 _Color;
}

常用函数

// 颜色纹理
tex2D(_MainTex, IN.uv_MainTex)
// 法线贴图
UnpackNormal(tex2D(_BumpMap, IN.uv_MainTex))
// 点乘
dot()
// 标准化
normalize()
// 限制在0-1
saturate()

四、顶点/片元着色器

4.1 基本结构

Shader "Custom/NewSurfaceShader" {Properties {}SubShader {Pass {// 设置渲染状态和标签// CGCGPROGRAM// 该代码片段的编译指令# pragma vertex vert# pragma fragment frag// CG代码ENDCG// 其他设置}// 其他Pass}// 其他SubShaderFallBack "Diffuse"
}

4.2 顶点着色器和片元着色器之间的通信方法

Shader "Custom/Test" {SubShader {Pass {CGPROGRAM#pragma vertex vert#pragma fragment fragstruct a2v {float4 vertex: POSITION;float3 normal: NORMAL;float4 texcoord: TEXCOORD0;};struct v2f {float4 pos: SV_POSITION;fixed3 color: COLOR0;};v2f vert(a2v v) {// 输出结构, 实现通信用v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5);return o;}fixed4 frag(v2f i): SV_Target {// 这里的i的数据对应上面的oreturn fixed4(i.color, 1.0);}ENDCG}}FallBack "Diffuse"
}

4.3 示例

4.3.1 漫反射效果
SubShader {Pass {Tags {"LightMode" = "ForwardBase"}CGPROGRAM// 顶点着色器编译指令// 每有一个顶点就执行一次#pragma vertex vert// 片段着色器编译指令// 每有一个像素就执行一次#pragma fragment frag#include "UnityCG.cginc"// 顶点输出结构体struct v2f {half4 screenPos: SV_POSITION;half3 screenNormal: NORMAL;};// 顶点输入结构体struct a2v {half4 vertex: POSITION;half3 normal: NORMAL;};v2f vert(a2v input) {v2f o;o.screenPos = UnityObjectToClipPos(input.vertex);// 将顶点法线转换到世界空间的法线, 为片元着色器准备o.screenNormal = mul(half4(input.normal, 1), unity_WorldToObject).xyz;return o;}// 每个像素点的颜色half4 frag(v2f input): COLOR {// 入射光线half3 lightDir = normalize(_WorldSpaceLightPos0.xyz);// 法线向量half3 normal = normalize(input.screenNormal);// 正向渲染公式: 入射光线颜色和强度 * 材质的漫反射系数 * Max(0, dot(光源方向, 表面法线))half4 diffuseColor = _LightColor0.rgb * _DiffuseColor.rgb * max(0, dot(lightDir, normal));// 最终结果加上环境光return half4(diffuseColor, 1) + UNITY_LIGHTMODEL_AMBIENT;}ENDCG}
}
4.3.2 单张纹理
Shader "Custom/MyShader" {Properties {_Color ("Color Tint", Color) = (1, 1, 1, 1)_MainTex ("Main Tex", 2D) = "white" {}_Specular ("Specular", Color) = (1, 1, 1, 1)// 高光范围_Gloss ("Gloss", Range(8.0, 256)) = 20}SubShader {Pass {Tags { "LightMode"="ForwardBase" }CGPROGRAM#pragma vertex vert#pragma fragment frag#include "Lighting.cginc"fixed4 _Color;sampler2D _MainTex;// 必须以纹理变量名+_ST来命名, .xy存储缩放值, .zw存储偏移值float4 _MainTex_ST;fixed4 _Specular;float _Gloss;struct a2v {float4 vertex : POSITION;float3 normal : NORMAL;float4 texcoord : TEXCOORD0;};struct v2f {float4 pos : SV_POSITION;float3 worldNormal : TEXCOORD0;float3 worldPos : TEXCOORD1;float2 uv : TEXCOORD2;};v2f vert(a2v v) {v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.worldNormal = UnityObjectToWorldNormal(v.normal);o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;// 下面两句效果相同o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;// o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);return o;}fixed4 frag(v2f i) : SV_Target {fixed3 worldNormal = normalize(i.worldNormal);fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));// 通过纹理来采样漫反射颜色fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;// 环境光fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));// 高光反射fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));fixed3 halfDir = normalize(worldLightDir + viewDir);fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);return fixed4(ambient + diffuse + specular, 1.0);}ENDCG}}FallBack "Diffuse"
}
4.3.3 凹凸映射

在切线空间下计算

Shader "Custom/MyShader" {Properties {_Color ("Color Tint", Color) = (1, 1, 1, 1)_MainTex ("Main Tex", 2D) = "white" {}_BumpMap ("Normal Map", 2D) = "bump" {}_BumpScale ("Bump Scale", Float) = 1.0_Specular ("Specular", Color) = (1, 1, 1, 1)_Gloss ("Gloss", Range(8.0, 256)) = 20}SubShader {Pass { Tags { "LightMode"="ForwardBase" }CGPROGRAM#pragma vertex vert#pragma fragment frag#include "Lighting.cginc"fixed4 _Color;sampler2D _MainTex;float4 _MainTex_ST;sampler2D _BumpMap;float4 _BumpMap_ST;float _BumpScale;fixed4 _Specular;float _Gloss;struct a2v {float4 vertex : POSITION;float3 normal : NORMAL;float4 tangent : TANGENT;float4 texcoord : TEXCOORD0;};struct v2f {float4 pos : SV_POSITION;float4 uv : TEXCOORD0;float3 lightDir: TEXCOORD1;float3 viewDir : TEXCOORD2;};v2f vert(a2v v) {v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);// 计算变换矩阵, 下面两句话相当于TANGENT_SPACE_ROTATION;fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; float3x3 worldToTangent = float3x3(worldTangent, worldBinormal, worldNormal);// 如果使用TANGENT_SPACE_ROTATION;, 那么就需要将这里的worldToTangent改为rotationo.lightDir = mul(worldToTangent, WorldSpaceLightDir(v.vertex));o.viewDir = mul(worldToTangent, WorldSpaceViewDir(v.vertex));return o;}fixed4 frag(v2f i) : SV_Target {				fixed3 tangentLightDir = normalize(i.lightDir);fixed3 tangentViewDir = normalize(i.viewDir);// 对法线纹理进行采样fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);fixed3 tangentNormal;// 如果不是法线贴图, 那么就需要下面两句来计算法线// tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;// tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));// 是法线贴图, 那么就需要这样来计算法线// 法线和像素的映射关系一般为 normal = 2 * pixel - 1tangentNormal = UnpackNormal(packedNormal);tangentNormal.xy *= _BumpScale;// 单位向量模长为1, 所以z = sqrt(1 - x ^ 2 - y ^ 2)tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;// 使用法线贴图的法线, 就可以有凹凸效果了fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss);return fixed4(ambient + diffuse + specular, 1.0);}ENDCG}} FallBack "Specular"
}

在世界空间下计算

Shader "Custom/MyShader" {Properties {_Color ("Color Tint", Color) = (1, 1, 1, 1)_MainTex ("Main Tex", 2D) = "white" {}_BumpMap ("Normal Map", 2D) = "bump" {}_BumpScale ("Bump Scale", Float) = 1.0_Specular ("Specular", Color) = (1, 1, 1, 1)_Gloss ("Gloss", Range(8.0, 256)) = 20}SubShader {Pass { Tags { "LightMode"="ForwardBase" }CGPROGRAM#pragma vertex vert#pragma fragment frag#include "Lighting.cginc"fixed4 _Color;sampler2D _MainTex;float4 _MainTex_ST;sampler2D _BumpMap;float4 _BumpMap_ST;float _BumpScale;fixed4 _Specular;float _Gloss;struct a2v {float4 vertex : POSITION;float3 normal : NORMAL;float4 tangent : TANGENT;float4 texcoord : TEXCOORD0;};struct v2f {float4 pos : SV_POSITION;float4 uv : TEXCOORD0;// 切线空间到世界空间的3x3变换矩阵float4 TtoW0 : TEXCOORD1;  float4 TtoW1 : TEXCOORD2;  float4 TtoW2 : TEXCOORD3; };v2f vert(a2v v) {v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;  fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);  fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);  fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; // 计算将方向从切线空间转换为世界空间的矩阵, w值存入世界空间坐标o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);return o;}fixed4 frag(v2f i) : SV_Target {float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));// 根据法线纹理来计算切线空间下的法线fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));bump.xy *= _BumpScale;bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));// 把法线变换到世界空间下bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));fixed3 halfDir = normalize(lightDir + viewDir);fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);return fixed4(ambient + diffuse + specular, 1.0);}ENDCG}} FallBack "Specular"
}
4.3.4 渐变纹理

五、内置文件和变量

可以引用内置文件来调用函数和变量

CGPROGRAM
// 引用内置文件
#include "UnityCG.cginc"
ENDCG

常用内置文件
在这里插入图片描述
UnityCG中常用的结构体
在这里插入图片描述
UnityCG中常用的函数
在这里插入图片描述

附录1:另Spine/Skeleton Shader支持透明度修改功能

该shader不能直接用赋值的方式进行设置透明度。因为在通常情况下材质图片都是有完全透明的部分的,这个时候如果直接在片元着色器中赋值透明度的话,原先是完全透明的部分就会显示为黑色。
这里的解决方法为另当前透明度 = 纹理透明度 × 设定的透明度,即:texColor.a = texColor.a * _Alpha。

Shader "Spine/Skeleton" {Properties {_Cutoff ("Shadow alpha cutoff", Range(0,1)) = 0.1[NoScaleOffset] _MainTex ("Main Texture", 2D) = "black" {}[Toggle(_STRAIGHT_ALPHA_INPUT)] _StraightAlphaInput("Straight Alpha Texture", Int) = 0[HideInInspector] _StencilRef("Stencil Reference", Float) = 1.0[HideInInspector][Enum(UnityEngine.Rendering.CompareFunction)] _StencilComp("Stencil Comparison", Float) = 8 // Set to Always as default// Outline properties are drawn via custom editor.[HideInInspector] _OutlineWidth("Outline Width", Range(0,8)) = 3.0[HideInInspector] _OutlineColor("Outline Color", Color) = (1,1,0,1)[HideInInspector] _OutlineReferenceTexWidth("Reference Texture Width", Int) = 1024[HideInInspector] _ThresholdEnd("Outline Threshold", Range(0,1)) = 0.25[HideInInspector] _OutlineSmoothness("Outline Smoothness", Range(0,1)) = 1.0[HideInInspector][MaterialToggle(_USE8NEIGHBOURHOOD_ON)] _Use8Neighbourhood("Sample 8 Neighbours", Float) = 1[HideInInspector] _OutlineMipLevel("Outline Mip Level", Range(0,3)) = 0///// 修改的部分 /////// 透明度_Alpha("Alpha", Range(0, 1)) = 1}SubShader {Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" }Fog { Mode Off }Cull OffZWrite OffBlend One OneMinusSrcAlphaLighting OffStencil {Ref[_StencilRef]Comp[_StencilComp]Pass Keep}Pass {Name "Normal"CGPROGRAM#pragma shader_feature _ _STRAIGHT_ALPHA_INPUT#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"sampler2D _MainTex;float _Alpha;float _Threshold;struct VertexInput {float4 vertex : POSITION;float2 uv : TEXCOORD0;float4 vertexColor : COLOR;};struct VertexOutput {float4 pos : SV_POSITION;float2 uv : TEXCOORD0;float4 vertexColor : COLOR;};VertexOutput vert (VertexInput v) {VertexOutput o;o.pos = UnityObjectToClipPos(v.vertex);o.uv = v.uv;o.vertexColor = v.vertexColor;return o;}float4 frag (VertexOutput i) : SV_Target {float4 texColor = tex2D(_MainTex, i.uv);///// 修改的部分 /////// 用原来的透明度乘以现在的透明度texColor.a = texColor.a * _Alpha;texColor.rgb *= texColor.a;return (texColor * i.vertexColor);}ENDCG}Pass {Name "Caster"Tags { "LightMode"="ShadowCaster" }Offset 1, 1ZWrite OnZTest LEqualFog { Mode Off }Cull OffLighting OffCGPROGRAM#pragma vertex vert#pragma fragment frag#pragma multi_compile_shadowcaster#pragma fragmentoption ARB_precision_hint_fastest#include "UnityCG.cginc"sampler2D _MainTex;fixed _Cutoff;struct VertexOutput {V2F_SHADOW_CASTER;float4 uvAndAlpha : TEXCOORD1;};VertexOutput vert (appdata_base v, float4 vertexColor : COLOR) {VertexOutput o;o.uvAndAlpha = v.texcoord;o.uvAndAlpha.a = vertexColor.a;TRANSFER_SHADOW_CASTER(o)return o;}float4 frag (VertexOutput i) : SV_Target {fixed4 texcol = tex2D(_MainTex, i.uvAndAlpha.xy);clip(texcol.a * i.uvAndAlpha.a - _Cutoff);SHADOW_CASTER_FRAGMENT(i)}ENDCG}}CustomEditor "SpineShaderWithOutlineGUI"
}

数据本地化技术

一、BinaryFormatter

使用BinaryFormatter来进行存储存档,是比较方便的一种方法。只需要一个序列化的类来存放想要保存或读取的数据,当想要存储或读取时调用BinaryFormatter来序列化或反序列化即可。

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;// 存档内容
[Serializable]
public class SaveData {internal string userName;
}// 存档的方式
public static void Save(string saveDataPath, SaveData saveData) {// 实例化一个BinaryFormatter对象BinaryFormatter binaryFormatter = new BinaryFormatter();// 如果文件不存在则创建文件if (!File.Exists(saveDataPath)) {File.Create(saveDataPath).Dispose();}// 将存在SaveData类的数据写入文件try {FileStream file = File.Open(saveDataPath, FileMode.Open);binaryFormatter.Serialize(file, saveData);file.Close();}catch (Exception e) {Debug.Log(e.Message);}
}// 读档的方式
public static void Load(string saveDataPath) {// 如果文件不存在则抛出异常if (!File.Exists(saveDataPath)) {throw new FileNotFoundException($"{saveDataPath} not found");}// 读取存档数据BinaryFormatter binaryFormatter = new BinaryFormatter();FileStream file = File.Open(saveDataPath, FileMode.Open);GameManager.curUserSaveData = (SaveData)binaryFormatter.Deserialize(file);file.Close();
}

二、XML

三、Json

解析Json文件有很多可以用的库,这里使用JsonUtility来进行Json文件读写。

// JsonUtility的包
using UnityEngine;// 用于存储json文件的信息类一定要进行序列化
[System.Serializable]
public class PlayerInfo {public string name;public int lives;public float health;
}// 读取文件的方法
// 先用ReadAllText读取路径为jsonPath的json文件
string data = System.IO.File.ReadAllText(jsonPath);
// 之后便可以用FromJson来解析这段字符串
PlayerInfo jsonData = JsonUtility.FromJson<PlayerInfo>(data);
// 这时就可以调用了
print(jsonData.name);
print(jsonData.lives);// 写入文件的方法
PlayerInfo pi = new PlayerInfo();
// ToJson直接放入类对象即可
string json = JsonUtility.ToJson(pi);
System.IO.File.WriteAllText(jsonPath, json);

四、ScriptableObject


参考资料

[1] 菜鸟教程. C# 教程[EB/OL]. https://www.runoob.com/csharp/csharp-tutorial.html.
[2] Microsoft. System Namespace[EB/OL]. https://learn.microsoft.com/zh-cn/dotnet/api/system?view=net-8.0.
[3] 冯乐乐. Unity Shader入门精要[M]. 北京: 人民邮电出版社, 2016.

http://www.lryc.cn/news/2417628.html

相关文章:

  • 最新Node.js安装详细教程及node.js配置
  • base64编码解码器【C++】
  • Git指南(一)
  • IDE开发工具Idea使用
  • mysql知识点详细总结
  • 算法笔记(一)—— KMP算法练习题
  • vue-必备知识点,图文详解
  • VSCode初级使用教程详细版
  • 科技概念/名词解释
  • 计算机毕设ssm基于BS的高校学生毕业去向管理系统的设计与实现3l47e9(源码+数据库+LW)
  • 多吉搜索不能用了_百度搜索骚技巧,瞬间找到你想要的资源
  • 关于调用静态链接库LIB,提示重定义或库冲突的错误
  • saltstack-redhat6.5版本
  • 难倒了N个硕士和博士的三年级奥数题
  • 博客(Blog)的商业价值实现模式探讨
  • 使用hydra离线破解windows密码
  • 在Windows中使用TCP端口139和445
  • InfoQ-China发布在即,敬请关注支持
  • HttpServletRequest和ServletRequest的区别以及HttpServletRequest对象方法的用法
  • 【Consul】Consul的Linux之旅:实现高效部署与维护的入门指南
  • 华为荣耀5a是android几,荣耀5A配置怎么样 荣耀畅玩5A参数详情
  • vSphere 性能优化方法 故障排错方法及工具总结(一)
  • 抖音垂直养号,关键词养号,autojs脚本自动
  • 一些医学网站
  • Loadrunner中对中文进行UTF-8转码
  • 【论文合集】Awesome Diffusion Models 2
  • 视频编码标准H.264/AVC
  • python 天天向上续以七天为周期_《天天向上》主持人高天鹤回应考试作弊后续:以后要靠自己去拼搏...
  • 渗透测试知识点总结(非常详细),从零基础入门到精通,看完这一篇就够了
  • 分享一些好用的网站