Unity项目开发※
主讲人:唐寅权
学习目标:
- 掌握Unity3D进行游戏项目、虚拟现实项目、数字孪生项目的开发能力
- 了解企业开发习惯、规范变成习惯、跟企业无缝衔接
- 通过项目优化能力的培养,进行项目优化改进、提升项目性能
- 熟练掌握项目开发的整个流程以及各部门之间的配合
学习内容:
- C#相关内容学习以及对应的数据结构和算法
- Unity引擎各功能模块以及对应API
- 项目开发过程中常用的设计模式
- 项目开发资源管理和代码的组织方式
0x01 背景※
Unity为哪些行业带来了新的解决方案※
- 游戏开发:Unity可以用于创建2D和3D游戏,支持各种游戏类型,如动作游戏、射击游戏、角色扮演等
- 教育行业:开发交互式教育应用程序、模拟器、虚拟实验室等
- 医疗保健行业:医学培训模拟器、手术模拟器等
- 建筑和房地产行业:
- 汽车行业
- 航天航空行业
- 娱乐行业
- 广告和营销行业
Unity特点※
- 游戏开发:Unity可以用于创建2D和3D游戏,支持各种游戏类型,如动作游戏、射击游戏、角色扮演等
- 跨平台:移动端(Android、IOS) PC(Windows、MacOS、Linux)主机(PS、Xbox)虚拟现实VR、增强现实AR等
- 强大的编辑器:场景编辑、资源管理、物理引擎、动画编辑等功能
- 脚本支持:支持多种编程语言,如C#、JavaScript(现为UnityScript)等
- 生态系统:资源商店、文档、教程、社区支持等
安装Unity※
登入Unity中国区官网,下载Unity安装器UnityHub (Windows、MacOS、Linux)安装引擎
没有账号,需要注册个账号,有账号的直接登入,就会自动下载UnityHub
按照安装指引安装软件,Archlinux可以安装unityhub-cn
安装好UnityHub后,打开UnityHub,选择Installs标签页,安装官方最新release版本
可以选择LTS版本、最新版本、等,带c1后缀是中国版本
0x02 开始动手※
- 创建项目选择3D模板
- 更改Project Name
- 选择位置
- 创建

Unity3D 基本操作※
视图操作※
Scene窗口
| 快捷键 | 功能 |
| Alt+鼠标左键 | 以物体为中心、围绕物体进行观察 |
| F | 聚焦物体 |
| Q | 视图工具,平移整个Scene视图 |
| W | 移动工具,移动场景中所选的对象 |
| E | 旋转工具。按任意角度旋转场景中的对象 |
| R | 缩放工具,缩放场景中所选对象 |
| T | 横切面工具沿着横切面缩放场景中所选对象 |
| Z | 轴心工具,改变游戏对象轴心点 |
| X | 坐标系变换工具,本地坐标和世界坐标间切换 |
| Ctrl+Shift+F | 摄像机对齐到当前窗口 |
Unity使用左手坐标系,坐标单位为米
编程模式※
编程模式※
面向过程编程※
面向过程编程是一种基于解决问题步骤的编程范式,其中程序主要由一系列按照顺序执行的命令和函数组成。在面向过程编程中,重点是算法和函数的设计,而不是将数据和行为封装在对象内部。
在面向过程编程中,程序主要由函数组成,每个函数负责执行特定的任务。数据通常是分散存储
的,函数可以直接操作这些数据。面向过程的代码通常具有以下特点:
- 顺序执行:程序按照代码编写的顺序依次执行命令和函数,逐步完成任务。
- 函数:函数是面向过程编程的核心,用于执行特定的操作或任务。函数可以接受参数和返回值。
- 数据和函数分离:数据和函数通常是分开处理的,函数直接操作数据,实现特定的功能。
面向过程编程适用于简单的、直线的问题解决方案,对于复杂的系统和大型项目来说,面向过程编程可能会导致代码难以维护、理解性差等问题。相比之下,面向对象编程更适合于需要更好组织代码、提高复用性和可维护性的项目。
在Unity中,虽然更倾向于使用面向对象编程和面向组件编程的方式来开发游戏,但也可以在某些情况下使用面向过程的思维来解决问题。例如,在一些简单的功能实现或工具编写时,面向过程的方式可能更加直接和高效。
面向对象编程※
在Unity中,面向对象编程(OOP)是一种常见的编程范式,让开发者可以通过创建对象、定义类和使用继承等方式来组织和实现代码逻辑。在Unity中使用C#编程语言进行面向对象编程时,以下是一些常见的概念和实践:
- 类与对象:类是一种抽象数据类型,用于定义对象的属性和行为。对象是类的实例。在Unity中,游戏对象(GameObject)就是类的实例,可以通过脚本(Script)定义游戏对象的属性和行为。
- 封装:封装是一种将数据和行为封装在类内部,并通过公有接口暴露给外部的机制。在Unity中,可以使用访问修饰符(public、private、protected等)来控制属性和方法的可访问性。
- 继承:继承是一种类之间的关系,子类可以继承父类的属性和方法,并可以扩展或重写这些内容。在Unity中,可以使用继承来实现不同游戏对象之间共享行为或属性的功能。
- 多态:多态是指不同类的对象可以对相同的消息作出响应,实现了统一的接口。在Unity中,多态可以通过虚方法、抽象类、接口等概念来实现。
通过面向对象编程,Unity开发者可以更好地组织代码、提高代码复用性和可维护性,从而更高效地开发出功能强大的游戏。
一句话总结:封装用于隐藏对象的内部实现细节,继承用于实现类之间的层次关系,多态用于实现对象的多种形态。
面向组件编程※
在Unity中,面向组件编程是一种常见的编程风格,它基于Unity的组件化体系结构。在Unity中,游戏对象(GameObject)是通过将组件(Component)挂载到其上来实现功能和行为的。
面向组件编程在Unity中的工作方式如下:
- 每个游戏对象都可以有多个组件。
- 每个组件可以包含特定的功能或行为,例如渲染器(Renderer)、碰撞器(Collider)、脚本(Script)等。
- 开发者可以通过编写脚本来定义组件的行为和交互逻辑
- 组件之间可以相互影响和通讯,实现复杂的游戏逻辑
面向组件编程在Unity中的优点包括:
- 可重用性:组件可以在不同的游戏对象之间重复使用。
- 易于维护:通过将功能拆分成组件,易于单独管理和修改。
- 灵活性:可以动态添加、删除和修改组件,方便地调整游戏对象的功能和行为。
总的来说,Unity中的面向组件编程使得开发者可以更轻松地构建复杂的游戏系统,提高了开发效率和可维护性。
unity中的编程模式※
在Unity中,通常会将面向对象编程、面向组件编程和面向过程编程结合起来,以便充分利用各种编程范式的优势来开发游戏。下面是一些方法可以在Unity项目中结合这三种编程风格:
- 使用类和对象来组织游戏中的逻辑和数据,实现代码的重用性和可维护性。
- 定义游戏对象的属性和行为,如玩家、敌人、道具等。
- 使用继承、多态和封装等面向对象的概念来构建游戏系统的层级结构。
面向组件编程
- 利用Unity的组件化体系结构,将功能和行为分解为独立的组件并挂载到游戏对象上
- 通过组件之间的交互实现复杂的游戏逻辑,提高灵活性和可拓展性
- 编写组件类来定义特定功能,如碰撞检测、移动控制、动画播放等。
面向过程编程
- 在处理简单、线性的问题时,可以采用面向过程的方式来实现相关功能。
- 在编写一次性、独立的任务或功能时,可以使用面向过程编程风格。
- 可以结合使用函数来执行特定任务,保持代码清晰简洁。
通过结合这三种编程风格,开发者可以根据具体需求和场景选择合适的方式来编写代码,充分利用各种方法的优势。例如,可以使用面向对象编程来定义游戏对象及其行为,使用组件来实现复杂的功能模块,而在某些简单的功能实现上则可以使用面向过程的方法。这样可以使代码逻辑更清晰、易于维护,并提高开发效率。
规范※
命名规范※
- 英文单词命名。禁止使用拼音或无意义的字母命名。
- 直观易懂。使用能够描述其功能或有意义的英文单词或词组。
- 不要采用下划线命名。
- 常量、静态字段、类、结构体、非私有字段、方法等名称采用大驼峰式命名法。
- 私有字段、方法形参、局部变量采用小驼峰式命名法。
- 私有字段以下划线开头。
- 接口命名以大写字母I开头。
- 枚举以大写字母E开头。
int car_type // 错误:下划线命名。
public const float MaxSpeed = 100f; // 常量
public static float MaxSpeed = 100f; // 静态字段
public class GameClass; // 类名
public struct GameStruct; // 结构体
public string FirstName; // public字段
protected string FirstName; // protected字段
public void SendMesssage(string message); {} // 方法
private string _firstName; // 私有字段
public void FindFirstName(string firstName); // 方法参数
string firstName; // 局部变量
public interface IState; // 接口
public enum EGAmeType{Simple,Hard} // 枚举以及枚举值编码规范※
- 声明变量时,一行只声明一个变量。
- 类的字段声明统一放置于类的最前端。
- 一行代码长度不要超过屏幕宽度。如果超过了,将超过部分换行。
private string _firstName;
private string _lastName;
public class Student
{
private string _firstName;
private string _lastName;
public string GetFirstName()
}注释规范※
- 公共方法注释,采用///形式自动产生XML标签格式的注释。包括方法介绍、参数含义、返回内容。私有方法可以不注释。
- 公共字段注释,采用///形式自动产生XML标签格式注释。
- 私有字段注释,注释位于代码后面,中间Space隔开。
- 方法内的代码块注释。
/// <summary>
/// 设置场景名称
/// </summary>
/// <param name=“sceneName”>场景名</param>
/// <retuens>如果设置成功返回TRUE</returns>
public bool SetSceneName(string sceneName)
{
}0x03 C#基础※
流程控制※
流程控制※
一、条件语句※
1. if 语句:※
用于基于条件执行代码块。
if (condition)
{
// 当条件为真时执行的代码块
}2. if-else 语句:※
在 if 条件不满足时执行另一个代码块。
if (condition) {
// 当条件为真时执行的代码块
}
else
{
// 当条件为假时执行的代码块
}3. else-if 语句:※
用于处理多个条件情况。
if (condition1)
{
// 处理条件1的代码块
}
else if (condition2)
{
// 处理条件2的代码块
}
else
{
// 当所有条件都不满足时执行的代码块
}二、循环语句※
1. for 循环:※
用于执行一段代码块多次。
for (int i = 0; i < limit; i++)
{
// 循环体的代码块
}2. while 循环:※
在条件为真时重复执行一段代码块。
while (condition)
{
// 循环体的代码块
}3. do-while 循环:※
与 while 循环类似,但在检查条件之前至少执行一次循环体。
do
{
// 循环体的代码块
} while (condition);三、开关语句※
1. switch-case 语句:※
用于根据表达式的值选择不同的执行路径。
switch (expression)
{
case value1:
// 执行逻辑1
break;
case value2:
// 执行逻辑2
break;
default:
// 默认情况下的逻辑
break;
}四、跳转语句※
在 C# 中,跳转语句用于改变程序执行流程,可以让程序在不按顺序执行的情况下跳转到指定的代码段。
以下是常见的跳转语句:
1 . brake:※
通常用于循环或 switch 语句中,用于终止当前循环或 switch 语句的执行,并跳
出该代码块。
for (int i = 0; i < 10; i++)
{
if (i == 5)
{
break; // 当 i 等于 5 时跳出循环
}
Console.WriteLine(i);
}2 . continue:※
用于跳过当前循环中剩余的代码,直接进入下一次循环迭代。
for (int i = 0; i < 10; i++)
{
if (i == 5)
{
continue; // 当 i 等于 5 时跳过本次循环
}
Console.WriteLine(i);
}3 . return:※
语句用于从当前方法返回结果并结束方法的执行。可以在任何方法中使用。
int Add(int a, int b)
{
return a + b; // 返回 a 和 b 的和,结束方法的执行
}4 . goto:※
可以用来无条件地将执行跳转到代码中的指定标签处。通常不推荐使用,因为会使代码难以理解和维护。
int i = 0;
startPoint:
Console.WriteLine(i);
i++;
if (i < 10)
{
goto startPoint; // 无条件跳转到 startPoint 处继续执行
}5 . throw:※
用于抛出异常,可以在任何位置抛出特定的异常,中断当前代码流程。
if (errorCondition)
{
throw new Exception("An error occurred"); // 抛出异常并中断程序执行
}运算符※

运算符用于执行程序代码运算,会针对一个以上操作数进行运算。主要的运算符分类有:算术运算符,赋值运算符,关系运算符,逻辑运算符和位运算符。

1、逗号运算符(,)※
多个表达式可以用逗号分开,其中用逗号分开的表达式的值分别结算,但整个表达式的值是最后一个表达式的值。
int a1,a2,b=2,c=7,d=5;
a1=(++b,c—,d+3);
a2=++b,c—,d+3;代码依次执行下来,对于给a1赋值的代码,有三个表达式,用逗号分开,所以最终的值应该是最后一个表达式的值,也就是(d+3)的值为8。
对于给a2赋值的代码,也是有三个表达式,这时的三个表达式为a2=++b、c—、d+3。因为赋值运算符比逗号运算符优先级高,所以a2值为4。
2、算术运算符(+, - ,, /,%)※
算术运算符,就是用来处理四则运算的符号,这是最简单也最常用的符号,处理数字的时候几乎都会用到它。
它们的运算顺序是先乘除,后加减,如果有括号就先算括号内再算括号外,同一级运算顺序是从左到右。
注意:除法运算符的结果为两个操作数中的最高精度
3、赋值运算符(=,+=,-=,=,/=,%=,>>=,<<= ,&=, |= ,^=)※
(1)简单赋值运算符=※
它的作用是将一个表达式的值赋给一个左值。当左、右操作数的类型不同时,编译器会隐式进行转换,将右操作数转为左操作数。
int a, b, c;
a=3;
b=4;
c = ( a + b )(2a - b)// c结果为14(2)复合的赋值运算符+=,-=…※
例如:
a+=5 等价于a=a+5
x=y+7 等价于x=x(y+7)
r%=p 等价于r=r%p4、自增自减操作符(++,—)※
单目运算符,自增运算符 ++ 使操作数的值加1,其操作数必须为可变左值(可简单地理解为变量),自增运算符++ 可以置于操作数前面,也可以放在后面,如下
int num=0;
++num;
num++ ;++num表示,num自增1后再参与其它运算;
而num++ 则是num参与运算后,num的值再自增1
(1)不参与其它运算情况※
int i=3;
int j=4;
i++;
++j;i,j结果就是 4,5
(2)参与其它运算情况※
int i=3;
int j=4;
int a = i++;
int b = ++j;这里就开始体现出++前置与后置的区别了,a,b结果是3,5
5、关系运算符(<,<=,>, >= , ==,!=)※
用来判断两个操作数之间的关系

关系运算符的值只能是真和假
注意:在C++中,将非零值和true都判断为真。将0和false都判断为假。在C#中真和假只能true和false。
6、逻辑运算符(或真且假)※
逻辑或:||
逻辑与:&&
逻辑非:!
逻辑运算的数据和逻辑运算符的运算结果是boolean类型。
7、条件操作符(?:)※
唯一的三目运算符
使用形式:条件表达式?表达式成立的结果 : 表达式不成立的结果
如下是求两个变量中最大值:
int a=10;
int b=20;
int max=(a>b)?a:8、位运算符※
按位于:&
按位或:|
按位异或:^
按位取反:~
左移:<<
右移:>>
运算符用来对二进制位进行操作,对于位运算符来说,左右操作数均为整数,才有意义。
(1)按位与(&)※
按位与运算&是双目运算符,其功能是参与运算的两数各对应的二进制位进行逻辑与。只有对应的两个二进位均为1时,结果位才为1 ,否则为0。参与运算的数补码方式出现。
例如:9&5可写算式如下:
00001001 (9的二进制补码)
& 00000101 (5的二进制补码)
-------------------------
00000001 (1的二进制补码)(2)按位或(|)※
按位或运算符“|”是双目运算符, 其功能是参与运算的两数各对应的二进制位进行逻辑或。只要对应的二个二进位有一个为1时,结果位就为1。参与运算的两个数均以补码出现。
如:93 | 57
0000 0000 0101 1101
| 0000 0000 0011 1001
-----------------------
0000 0000 0111 1101(3)按位异或(^)※
只有在两个比较的位不同时其结果是1,否则结果为0,即“相同为0,不同为1”。
如:93 | 57
0000 0000 0101 1101
^ 0000 0000 0011 1001
----------------------
0000 0000 0110 0100实现两个数的交换:int a = 8;int b = 12;a=a^b;b=b^a;a=a^b;
(4)按位取反 (~)※
二进制每一位取反,0变1,1变0
如:
int a=93;
~a;
0000 0000 0101 1101 —->1111 1111 1010 0010(5)左移(<< )※
双目运算符,第一运算对象是移位对象,第二个运算对象是所移的二进制位数。
移位运算符是将数据看成二进制数,对其进行向左移动若干位的运算。左移一位相当于该数乘以2,左移N位相当于该数乘以2的N次方。
移位时,移出的位数全部丢弃,移出的空位补入的数与左移还是右移有关。如果是左移,则规定补入的数全部是0;如果是右移,还与被移位的数据是否带符号有关。若是不带符号数,则补入的数全部为0;若是带符号数,则补入的数全部等于原数的最左端位上的原数(即原符号位)。
移位时
1:移出的位数全部丢弃
2:移出的空位补0
93 << 3
0000 0000 0101 1101=>0000 0010 1110 1000
(6)右移操作符(>> )
※
双目运算符,第一运算对象是移位对象,第二个运算对象是所移的二进制位数。
移位运算符是将数据看成二进制数,对其进行向右移动若干位的运算。右移一位相当于除以2,右移N位相当于除以2的N次方。
移位时
1:移出的位数全部丢弃
2:移出的空位如果是整数补0,负数补1
93 >> 3
0000 0000 0101 1101=> 0000 0000 0000 1011
数组※
在 C# 中,数组是一种存储相同类型数据元素的数据结构,可以方便地对一组数据进行管理和操作。以下是关于数组的基本用法:
声明和初始化数组:※
可以使用以下语法声明和初始化数组:
// 声明并初始化整数数组
int[] numbers = new int[5]; // 创建一个包含5个整数元素的数组
// 直接初始化数组内容
int[] numbers = new int[] { 1, 2, 3, 4, 5 };
// 简化的写法
int[] numbers = { 1, 2, 3, 4, 5 };
// 二维数组
int[,]=new int[2,3];
// 多维数组
int[,,] = new int[2,3,4];
// 不定项数组(锯齿型数组):每行大小不一,需要两步才能申请空间
int[ ][ ] arr10 = new int[9][]; // 申请一维空间
for(int i = 0; i < arr9.Length; i++)
{
arr[9][i] = new[i+1]; // 申请二维空间
}
// 注意:下标不能越界访问数组元素:※
可以使用索引访问数组元素,索引从 0 开始:
int[] numbers = { 10, 20, 30, 40, 50 };
int secondNumber = numbers[1]; // 访问第二个元素,值为 20遍历数组:※
可以使用循环结构来遍历数组中的元素:
int[] numbers = { 10, 20, 30, 40, 50 };
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine(numbers[i]);
}多维数组:※
除了一维数组外,还可以创建多维数组:
注意:C#用[ , ]区分数组维度
int[,] matrix = new int[3, 3]; // 3x3 的二维数组
// 初始化二维数组
int[,] matrix = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } };
使用数组方法:※
数组对象有一些方法可以方便地进行操作,比如排序、查找等:
int[] numbers = { 3, 1, 4, 6, 2, 5 };
Array.Sort(numbers); // 对数组进行排序
int index = Array.IndexOf(numbers, 4); // 查找元素 4 的索引位置数组长度:※
可以使用 Length 属性获取数组的长度(元素个数):
int[] numbers = { 1, 2, 3, 4, 5 };
int length = numbers.Length; // 获取数组长度为 5数组在 C# 中是非常常用的数据结构,能够有效地管理和操作一组数据,对于存储多个相同类型的数据非
常方便。通过灵活使用数组,你可以更高效地处理和操作数据集合。
值类型和引用类型※
一、值类型和引用类型※
在 C# 中,数据类型可以分为值类型(Value Types)和引用类型(Reference Types),它们有着不同
的特点和行为。以下是值类型和引用类型的区别:
1. 值类型(Value Types):※
- 值类型直接包含数据本身,存储在栈(Stack)上。
- 值类型包括整数类型(int、long)、浮点数类型(float、double)、字符类型(char)、结构体(struct)、枚举(enum)。
- 值类型的变量是值的拷贝,修改一个变量的值不会影响到另一个变量。
int a = 10;
int b = a;
b = 20;
// 此时a仍然是10,b变为20
2. 引用类型(Reference Types):※
- 引用类型存储在堆(Heap)中,变量存储的是对象的引用(地址)。
- 多个变量可以引用同一个对象,对一个变量做修改会影响到其他引用同一对象的变量。
- 具体的引用类型包括类(class)、接口(interface)、委托(delegate)以及字符串(string)等。
- 传递引用类型变量时,传递的是引用的副本,所以多个引用指向的是同一个对象。
List<string> list1 = new List<string>();
List<string> list2 = list1;
list1.Add("A");
// list2 也会包含 "A",因为它们引用同一个 List<string> 对象3. 值类型和引用类型的内存分配:※
- 值类型的内存分配是在栈上分配,操作简单高效,但不能动态分配内存。
- 引用类型的内存分配是在堆上分配,动态分配和释放内存,需要进行垃圾回收(GarbageCollection)。
4. 结合值类型和引用类型的注意事项:※
- 在传递值类型时,传递的是值的副本,不会影响原始值。
- 在传递引用类型时,传递的是引用的副本,会影响对象的状态。
在编写 C# 代码时,了解值类型和引用类型的特点和区别,有助于避免一些潜在的问题并更好地理解代码执行过程。
二、结构体※
在 C# 中,结构体(Struct)是一种用户自定义的值类型(Value Type),它可以包含多个不同类型的字段(Fields)。结构体是轻量级的数据结构,适合用于简单的数据封装和传递,相对于类(Class)而言,结构体更适合用于存储小型的数据集合。以下是一些关于 C# 结构体的基本知识:
1. 结构体声明:※
结构体的声明使用 struct 关键字,通常位于命名空间内或其他类的内部。
public struct Person
{
public string name;
public int age;
}2. 结构体特点:※
- 结构体是值类型,存储在栈上,而类是引用类型,存储在堆上。
- 结构体没有继承性,不能包含默认构造函数,不能用作基类,也不能成为其他类的基类。
- 结构体可以包含字段、属性、方法、构造函数等成员。
3. 结构体实例化:※
可以使用 new 关键字来实例化结构体,也可以直接声明并赋值。
(结构体的实例化不代表分配内存,仅仅只是执行构造函数)
(结构体中存在默认构造函数,但不能显式的定义,可以构造有参数的构造函数)
Person person1 = new Person();
person1.name = "Alice";
person1.age = 30;
Person person2 = new Person { name = "Bob", age = 25 };4. 结构体与类的比较:※
- 结构体适合用于小型的数据封装和传递,无需频繁的实例化和销毁。
- 结构体在参数传递时是按值传递,而类是按引用传递。
- 使用结构体可以避免引用类型的一些问题,如空引用异常等。
5. 注意事项:※
- 结构体适合用于简单的数据表示,不适合用于包含复杂逻辑和大量数据的场景。
- 结构体占用的内存较少,但大量的结构体实例化会增加栈内存的使用,需要谨慎使用。
通过合理地使用结构体,你可以更高效地组织和管理数据,尤其是在需要频繁传递和处理小型数据集合
的情况下。
三、枚举※
在 C# 中,枚举(Enumeration)是一种用于定义命名常量集合的数据类型。枚举可以帮助代码更具可读
性和可维护性,避免使用魔术数字。以下是关于 C# 中枚举的基本知识:
1. 枚举声明:※
枚举使用 enum 关键字声明,定义了一组命名的常量。枚举成员的值从 0 开始递增,可以显式指定初始值。
public enum DaysOfWeek
{
Monday,// 0
Tuesday,// 1
Wednesday, // 2
// 可以继续列举其他成员
}2. 枚举使用:※
可以通过枚举名称和成员名称来访问枚举常量。
DaysOfWeek day = DaysOfWeek.Monday;
Console.WriteLine(day); // 输出: Monday
int dayValue = (int)DaysOfWeek.Tuesday; // 转换为整数3. 指定枚举成员的值:※
可以显式指定枚举成员的值,后续成员的值会自动递增。
public enum Status
{
InProgress = 1,
Completed = 2,
Canceled = 3
}4. 枚举标志(Flags):※
使用 [Flags] 特性可以将枚举用作标志位,可以组合多个值。
[Flags]
public enum Permissions
{
Read = 1,
Write = 2,
Delete = 4
}
Permissions userPermission = Permissions.Read | Permissions.Write;
bool hasWritePermission = (userPermission & Permissions.Write) ==Permissions.Write;5. 枚举转换:※
- 可以使用强制类型转换将枚举转换为整数等类型。
- 可以使用 Enum.Parse() 方法将字符串转换为枚举值。
int value = (int)DaysOfWeek.Wednesday; // 转换为整数
DaysOfWeek Friday = (DaysOfWeek)Enum.Parse(typeof(DaysOfWeek), "Friday");6. 注意事项:※
- 枚举默认基础类型为 int ,可以显式指定其他整数类型。
- 枚举的成员名称必须唯一,但值不需要唯一。
- 可以将枚举嵌套在类或结构中。
通过合理使用枚举,可以使代码更清晰易懂,并提高代码的可维护性。结合枚举可以使代码中的常量更具有语义性。
0x04 组件※
Transform (3D):控制物体在世界的位置、旋转和缩放。(必要组件)
Rect Transform (2D)
Mesh Filter:管理物体顶点结构
Mesh Renderer:渲染器,渲染物体
Collider:碰撞器
Rigidbody:刚体
新建C# Script 模板
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 装备类
/// </summary>
public class Equip
{
// 数据成员
int id;
string name;
float price;
// 函数成员
}
/// <summary>
/// 类的结构
/// 1 数据成员
/// 1.1 基本数据成员
/// 整数:byte、short、int、long
/// 小数:float、double
/// 字符:char
/// 布尔:bool
/// 1.2 复杂数据成员
/// 字符串:string
/// 结构体:struct
/// 枚举:enum
/// 类:class
/// 委托:deleget
/// 2 函数成员
/// </summary>
public class ClassName : MonoBehaviour
{
// 数据成员
uint level; // 等级
double money; // 钱
float hp; // 血量
bool isDead; // 是否死亡
string name; // 名字
// 函数成员
Equip equip; // 装备,默认为null
// Start is called before the first frame update
// 初始化函数,Play时只执行一次
void Start()
{
// 操作数据成员
// 定义数据,不初始化有默认值
// 低精度可以给高精度赋值,高精度不能给低精度赋值
// 高精度给低精度赋值使用强制类型转换,会丢失精度
level = 1;
hp = 100f;
money = 9999;
exp = 0.0;
idDead = false;
name = "刀"
equip = new Equip();
print("level:"+level);
}
// Update is called once per frame
// 每一帧执行一次
void Update()
{
}
}0x05 获取对象/组件※
一、 获取对象※
在 Unity 中获取游戏对象有多种方式,以下是一些常用的获取游戏对象的方式:
1 使用名称获取
- 使用 GameObject.Find() 方法按照名称获取游戏对象。这个方法会在场景中查找并返回名称
匹配的第一个游戏对象。名称中可以添加上路径。
// 按照名称获取名为 "Cube" 的游戏对象
GameObject cube = GameObject.Find("Cube");2 按标签获取
- 使用 GameObject.FindGameObjectsWithTag() 方法按照标签获取游戏对象。这个方法会返
回所有标记为指定标签的游戏对象数组。
// 获取所有标记为 "Enemy" 的游戏对象
GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");3 通过变量引用获取
- 在脚本中声明公共变量并在 Unity 编辑器中拖拽关联游戏对象到这些变量上,以便直接访问。
public GameObject player; // 在脚本中声明一个公共游戏对象变量
void Start()
{
// 在 Unity 编辑器中将 Player 游戏对象拖拽到 Player 变量上
player.GetComponent<PlayerController>().Move(); // 通过变量引用直接访问游戏对象及其组件
}4 通过遍历获取
- 使用 GameObject.FindObjectsOfType<T>() 方法获取场景中所有指定类型的游戏对象数
组。
// 获取场景中所有类型为 Rigidbody 的游戏对象数组
Rigidbody[] rigidbodies = GameObject.FindObjectsOfType<Rigidbody>();以上除了变量引用物体,其它方式都不支持隐藏物体的查找。如需获取隐藏物体,使用transform.find函数
二、组件获取※
在 Unity 中获取游戏对象的组件(Component)有多种方式,以下是一些常用的获取游戏组件的方式:
1 在同一对象上获取组件
- 使用 GetComponent<T>() 方法在同一游戏对象上获取指定类型的组件。
// 获取当前游戏对象上的 Rigidbody 组件
Rigidbody rb = GetComponent<Rigidbody>();2 在其他对象上获取组件
- 使用 GetComponentInChildren<T>() 或 GetComponentInParent<T>() 方法在子对象或父对象中查找并获取指定类型的组件。
// 获取当前游戏对象的子对象中的脚本组件
MyScript myScript = GetComponentInChildren<MyScript>();
// 获取当前游戏对象的父对象中的碰撞体组件
Collider parentCollider = GetComponentInParent<Collider>();3 通过变量引用获取
- 类似于获取游戏对象,你也可以在脚本中声明公共变量并在 Unity 编辑器中拖拽关联组件到这些变量上,以便直接访问。
public Rigidbody rb; // 在脚本中声明一个公共刚体变量
void Start()
{
rb.AddForce(Vector3.up * 10f); // 通过变量引用直接访问组件
}4 通过GetComponent()方法获取
- 结合获取游戏对象方式,也可以在获得游戏对象的同时获取指定类型的组件。
// 获取名为 "Cube" 的游戏对象,并尝试获取 Rigidbody 组件
GameObject cube = GameObject.Find("Cube");
Rigidbody cubeRb = cube.GetComponent<Rigidbody>();生命周期函数※
一、Monobehaviour生命周期函数※
在Unity中,MonoBehaviour是所有脚本的基类,它提供了一系列生命周期函数,这些函数在对象的生命周期中被自动调用。这些生命周期函数允许开发者在特定的时机执行代码,例如在对象被创建、启用、更新或销毁时。
以下是一些常用的MonoBehaviour生命周期函数:
1. Awake():※
在对象被创建时调用,用于初始化对象。它在所有其他生命周期函数之前调用,通常用于初始化变量或设置初始状态。
2. Start():※
在对象第一次被激活时调用。它在Awake()之后第一次Update()调用前调用,用于初始化一些需要在所有对象都已被创建后进行的操作。
3. Update():※
在每一帧更新时调用。通常用于处理游戏逻辑、输入检测、动画更新等。Update()函数会在游戏每一帧被调用,因此它是频繁执行的函数。
4. FixedUpdate():※
在固定时间间隔内调用。通常用于物理相关的操作,比如移动刚体、施加力等。FixedUpdate()的执行间隔由物理引擎决定,通常默认为每秒50次。
5. LateUpdate():※
在Update()函数执行完毕后调用。通常用于处理在Update()中进行修改后的逻辑,例如跟随相机移动、处理角色控制等。
6. OnEnable()和OnDisable():※
OnEnable()在对象激活时被调用,OnDisable()在对象被禁用时被调用
7. OnDestroy():※
当对象被销毁时调用。通常用于清理资源、取消订阅事件等释放操作。
8.OnCollisionEnter(Collision collision):※
当物体碰撞进入时调用
9. OnTriggerEnter(Collider other)※
当触发器被进入时调用
using UnityEngine;
public class ExampleScript : MonoBehaviour
{
private void Awake()
{
// 初始化操作
}
private void Start()
{
// 启动时的初始化操作
}
private void Update()
{
// 每帧更新操作
}
private void FixedUpdate()
{
// 每固定帧率更新操作
}
private void LateUpdate()
{
// 在所有Update调用后执行
}
private void OnDisable()
{
// 当脚本所附加的物体被禁用时执行
}
private void OnDestroy()
{
// 当脚本所附加的物体被销毁时执行
}
private void OnCollisionEnter(Collision collisionInfo)
{
// 当物体碰撞进入时执行
}
// 其他函数可以根据需要添加...
}
这些函数可以根据你的具体需求进行重写,以添加自定义的行为。记住,在重写这些函数时,要确保代码不会引起长时间的阻塞,因为这会影响Unity的主更新循环,导致帧率下降。
二、异步调用Invoke※
Invoke 函数是 MonoBehaviour 类中的一个方法,用于在指定时间后调用特定的方法,例如执行某些初始化操作、在一段时间后触发特定事件等。
它的一般形式如下:
public void Invoke(string methodName, float time);
public void InvokeRepeating(string methodName, float time, float repeatRate);
methodName参数是要调用的方法的名称。time参数是在多长时间后开始调用指定方法,单位为秒。repeatRate参数仅适用于 InvokeRepeating 方法,表示方法的调用重复间隔时间,单位为秒。
CancelInvoke 方法是用于取消通过 Invoke 或 InvokeRepeating 启动的调用计划的方法
public void CancelInvoke(string methodName) //取消名为MethodName的方法调用
public void CancelInvoke() //取消所有通过 Invoke或InvokeRepeating调用的方法下面是一个简单的例子,展示了 Invoke 函数的用法:
using UnityEngine;
public class Example : MonoBehaviour
{
void Start()
{
// 在2秒后调用名为 "MyMethod" 的方法
Invoke("MyMethod", 2.0f);
// 在5秒后每隔2秒重复调用名为 "RepeatedMethod" 的方法
InvokeRepeating("RepeatedMethod", 5.0f, 2.0f);
// 在10秒后取消所有通过 Invoke 或 InvokeRepeating 安排的方法调用计划
Invoke("CancelAllInvoke", 10.0f);
}
void MyMethod()
{
Debug.Log("MyMethod was called!");
}
void RepeatedMethod()
{
Debug.Log("RepeatedMethod was called!");
}
}在这个例子中, MyMethod 方法会在游戏启动后的2秒钟后被调用一次,而 RepeatedMethod 方法会在游戏启动后的5秒钟后开始被调用,然后每隔2秒钟被重复调用一次。
注意:无论是游戏对象失活或者是脚本组件失活都不会停止Invoke调用过的函数,除非脚本或者游戏对象销毁即可停止
三、协程函数※
在Unity中,协程(Coroutine)是一种特殊的函数,用于在一段时间内暂停执行,并在稍后的时间点继续执行。通常情况下,我们在代码中通过调用协程来实现一些需要延迟执行或需要分步处理的任务。协程在Unity中有如下特点:
- 可以在一帧中的不同时间点暂停和恢复执行,而不需要等待整个帧的结束。
- 可以方便地实现延迟执行,例如在几秒后播放音效或执行一个动画。
- 可以使用协程来实现复杂的异步任务,而不需要使用回调函数或依赖于线程
协程的使用如下所示:
1. 在脚本中定义协程函数,函数必须返回IEnumerator类型,并使用yield语句中断执行流程。※
yield语句的作用是暂停协程的执行,等待指定的时间或事件结束后再次执行协程。
private IEnumerator MyCoroutine()
{
// 执行一些操作
yield return new WaitForSeconds(2.0f);
// 等待 2 秒后执行下一步操作
Debug.Log("等待了2秒后继续执行");
yield return new WaitForSeconds(1.0f);
Debug.Log("又等待了1秒后继续执行");
}2. 在其他函数中通过 StartCoroutine() 启动协程函数并控制协程的开始和停止。※
例如:
private void Start()
{
// 启动协程函数 MyCoroutine()
StartCoroutine(MyCoroutine());
}
private void Update()
{
// 在 Update() 函数中控制协程的开始和停止
if (Input.GetKeyDown(KeyCode.Space))
{
// 停止协程函数 MyCoroutine()
StopCoroutine(MyCoroutine());
}
}3. 协程的挂起※
//程序在下一帧中从当前位置继续执行
yield return 0;
//程序在下一帧中从当前位置继续执行
yield return null;
//程序等待N秒后从当前位置继续执行
yield return new WaitForSeconds(N);
//在所有的渲染以及GUI程序执行完成后从当前位置继续执行
yield new WaitForEndOfFrame();
//所有脚本中的FixedUpdate()函数都被执行后从当前位置继续执行
yield new WaitForFixedUpdate();
//等待一个网络请求完成后从当前位置继续执行
yield return WWW;
//等待一个xxx的协程执行完成后从当前位置继续执行
yield return StartCoroutine(xxx);
//如果使用yield break语句,当不满足执行条件时直接从当前位置跳出函数体,回到函数的根部
yield break;4. 协程使用的场合:※
- 控制游戏对象的缓慢移动、旋转等动画效果。
- 实现计时器、等待操作等功能。
- 遍历复杂的数据结构或者进行其他需要分散处理的操作。
- 当协程的数量过多时,可能会对游戏资源造成较大的压力。
5. 协程和线程的区别※
- 执行方式:线程是由操作系统调度的最小执行单位,它在操作系统的控制下运行,可以并行执行。协程是由程序控制的执行单位,它由程序自己在协程调度器的管理下运行,可以通过协作方式实现并发。
- 并发性:线程可以在多个 CPU 核心上并行执行,利用多核优势提高计算效率。而协程通常在单个线程中运行,通过在不同协程间的切换实现并发,无法利用多核优势。
- 内存和资源消耗:线程在创建时需要分配独立的内存空间,包括堆栈等资源,同时线程切换时需要保存和恢复上下文,消耗较多的内存和资源。协程则可以在一个线程中存在多个,共享同一堆栈空间,切换时只需要保存和恢复少量上下文,消耗较少的内存和资源。
- 同步与通信:在线程中,由于存在资源竞争,需要使用锁机制等同步手段来保护共享资源,避免数据冲突。协程通过显式地让出执行权和恢复执行权来避免资源竞争,并通过通道(channel)等机制进行协程间的通信。