最近想补一补C#基础,Unity官方的C#中级编程教程质量很高,于是开个帖子把跟着敲+记录了部分价讲解和我自己的理解的代码存在这
原课程链接:添加链接描述
https://www.bilibili.com/video/BV1f5411G7bp?p=1&vd_source=cdfd0a0810bcc0bcdbcf373dafdf6a82
说明:
因为这些代码主要起的是类似备忘录的作用,我个人只是用来速查老师授课的知识点、配合程序方便理解,并不会把它们挂在场景内物体上运行功能,注释比代码多,所以为了可读性就采取了很多不规范的写法,比如说为了脚本名字和原视频标题对应起来用了中文命名类(正式项目千万别这样,中文很容易报错),以及把多个功能类写一起等(只有多个数据类才能共存在一个脚本里,每个继承Monobehaviour的功能类都应该单独一个脚本),在参考的时候不要模仿。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 属性 : MonoBehaviour
{
//目的:从类外访问成员变量
//方法1:public 公开
//方法2:使用属性(更好)
//属性本身可以当作变量,并且可以封装成员变量——称为字段
//通过这种封装,我们可以更好地掌握字段的访问时间和访问方式
private int experience;
//属性语法:访问修饰符(public)+类型(int)+名称首字母大写(Experience)
public int Experience//一个属性可以有两个访问器
{
get//在get访问器内,返回封装的字段
{
//这里也可以写一些别的功能
return experience;
}
set//在set访问器内,使用关键字value给字段赋值
{
//这里也可以写一些别的功能
experience = value;
}
}
public int Level
{
get
{
return experience / 1000;
//如果有一个字段(封装好的属性)代表经验值,就可以用属性来代表玩家的等级
//Level等级属性的get访问器可以返回expe字段除以1000的值,而不返回真实的经验值
//这样一来,它可以返回数字等级,而不是玩家拥有的经验数量
}
set
{
experience = value * 1000;
//set访问器用于接受等级和计算玩家所获得的经验数量,并将值存储在expe字段中
}
}
//属性的另一个特点是它们可以被自动实现
//要创建自动实现的属性可以使用简写语法,get和set访问器后面只跟一个分号
public int Health { get; set; }//通过这种方式创建的属性,行为与字段完全相同
//区别在于可以通过移除get或者set访问器,使属性只读或者只写
}
public class 属性使用 : MonoBehaviour
{
private void Start()
{
属性 feature = new 属性();
feature.Experience = 114514;
int x = feature.Experience;
int y = feature.Level;
feature.Level = 999;
}
//使用属性可以实现两项public公共变量无法实现的操作:
//1.通过省略get或者set,可以有效地将字段设置为只写或者只读
//如果字段是私有private的,没有get访问器则无法读取该字段,没有set访问器无法写入该字段
//2.访问器可以视为函数,可以在访问器get,set方法内部运行其他代码或者调用其他函数
}
//VS支持用代码段快速创建属性
//输入prop然后按下Tab键会自动插入代码段
//个人感觉这个适用于围绕一个核心属性(封装字段)延展的相关属性
//比如说根据玩家的金币数计算商品的最大可购买数量,根据世界等级高低修改掉落率,或者什么“每有1000点生命值,暴击率增加百分之1,最高不超过百分之10”的类似设置
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 三元运算符 : MonoBehaviour
{
//三元运算符是if-else语句的精简形式,用于根据布尔表达式在两个值之间做出选择
private void Start()
{
int health = 10;
string message;
//格式:布尔值 问号? true表达式 冒号: false表达式
message = health > 0 ? "Player is Alive" : "Player is Dead";
//三元运算符可以相互嵌套。但是如果用于长表达式可能会导致代码繁琐,难以理解
message=health>0? "Player is Alive":health==0? "Player is Barely Alive":"Player is Dead";
}
}
//使用三元运算符而非if语句的基本规则:
//代码需要简单的if-else结构,而且每种情况只需要一个短表达式
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 静态 : MonoBehaviour
{
//静态成员,如变量和方法,是跨类的所有实例共享的成员
//此外,静态成员可以直接通过类访问,无需先实例化类的对象
//通常,成员变量对于类的每个对象是唯一的
//虽然类的每个对象具有相同的变量,但他们各自有自己的值
//然而,对于静态变量,类的每个对象具有相同的变量和相同的值
//因此,如果在一处更改某个静态变量的值,则所有其他静态变量的值也将更改
public static int enemyCount = 0;//该类中实例化了多少个对象,static表明为静态的,属于类本身,而不属于类的任何实例
public 静态()
{
//每次创建这个类的对象时,都让这个变量递增
enemyCount++;
}
}
public class 使用静态变量
{
void Start()
{
静态 enemy1 = new 静态();
静态 enemy2 = new 静态();
静态 enemy3 = new 静态();
int x = 静态.enemyCount;//使用类的名称和点运算符来访问静态变量enemyCount
}
}
//感觉可以作为道具和敌人创建用,一个静态变量储存总数需要多少个,一个静态变量储存现在有多少个在场景里
//隔一段固定时间(设置好的刷新时间)对比这俩数量,如果发现现有场景里面的数量少了(可能是被玩家收集或者击杀了),就生成补足
//(不过生成的位置怎么确定呢??精确到坐标还是一定范围内随机生成呢?)
//该过程也是很做游戏对象组件的脚本
public class 使用静态变量组件脚本
{
//了解某个场景中创建的玩家数量
public static int playerCount = 0;//声明静态变量playerCount
//在start方法中,让这个变量递增
void Start()
{
playerCount++;
//现在,只要创建与这个脚本关联的游戏对象,玩家总数就会增加
//感觉适用于多人联网游戏
}
}
//在另一个脚本组件中,我们可以使用脚本的名称和点运算符来访问这个静态变量
public class 使用静态变量组件脚本的管理类
{
void Start()
{
int x = 使用静态变量组件脚本.playerCount;
}
}
//与静态变量一样,静态方法属于类,而不属于类的特定对象
public class 静态方法
{
public static int Add(int num1,int num2)//静态方法
{
return num1 + num2;
}
}
public class 使用类的静态方法
{
void Start()
{
//使用类的名称和点运算符来调用静态的类方法Add,无需实例化对象
int x = 静态方法.Add(5, 6);
}
//Input.GetAxis/GetKey/GetButton等方法都是静态方法
}
//注意,不能在静态方法内部使用非静态类成员变量
//记住,静态方法属于类,而非静态方法属于类的实例
//也可以让整个类成为静态,将static置于类名称的前面即可
public static class 整个类成为静态
{
public static int Add(int num1, int num2)//静态方法
{
return num1 + num2;
}
//结果:类是静态的,并且不能创建类的实例
}
//如果想要使类完全由静态成员变量和方法组成,如Input类,则这样在类的名称前面加static非常有用
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 方法重载 : MonoBehaviour
{
//为单个方法提供多重定义,使用同一个方法名称执行多种不同的操作
//比如,AddNumbers与AddStrings一个加数字,一个加字符串,本质上是一种方法,但是却要记两个名字
//更好的方法是重载名为Add的方法,使其处理数字或者字符串
public int Add(int num1,int num2)
{
return num1 + num2;
}
//每个方法都有签名,签名由方法发名称和参数列表组成
//在同一个作用域内,每个方法的签名都是唯一的
//重载方法的操作是为新方法指定相同名称但指定不同的签名
//继续之前的示例,我们可以重载Add这个方法,来创建一个将字符串相加的新方法
public string Add(string str1,string str2)
{
return str1 + str2;
}
}
public class 使用重载的方法:MonoBehaviour
{
private void Start()
{
方法重载 myClass = new 方法重载();
//将根据传入的参数选择正确的版本
myClass.Add(1, 2);
myClass.Add("Hello ", "world");
}
}
//当系统尝试确定要运行的正确的已重载方法版本时,可能会出现三种情况
//第一,Exact Match 与传入参数完全匹配,运行这个版本的已重载方法
//第二,Least Convers 如果不是完全匹配,系统将查看所有可能的匹配项,并将选择一个需要最少转换量的版本
//第三,Error 如果最后没有可能的匹配项,或者多个版本需要的转换量相同,则会抛出错误。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 通用_泛型 : MonoBehaviour
{
//泛型是一种特征,通过该特征,类型可以作为参数传递给类和方法等
//实际上,这允许在不了解所处理数据的确切类型的情况下进行一般编程
//我们之前已经看到过,GetComponent方法使用泛型参数来获取其所寻找的组件的类型
//GetComponent就是泛型方法
//泛型参数:名称为T,用尖括号括起来,置于方法名称之后,普通或者形参之前
//由于这个T可以代表任何类型,所以其名称是任意的,但是按照惯例字母T最为常用
public T GenericMethod<T>(T param)
{
return param;
//与这个方法关联的泛型类型是T,但T只是一个占位符,调用这个方法时,T最终会成为实际类型
//无论T成为什么类型,这个类型也会成为方法的返回类型和参数类型,因为她们都使用了T作为其类型
}
//名称任意的,不用T用A也行
public A TestForOtherName<A>(A param)
{
return param;
}
//同样,如果要添加多个泛型参数,可以使用都好继续添加
//命名惯例通常遵循T,之后的参数是U和V
//虽然泛型函数不仅限于三个参数,但是很少看到人们使用超过三个参数
public T GenericMethod<T,U,V>(T param1,U param2,V param3)
{
return param1;
}
//泛型类型的用途:
//由于我们不知道这个泛型类型的行为方式,所以能做的操作不多
//这个泛型参数可以是任意值,比如浮点数、模型行为等
//由于我们不知道它是什么,所以能对它执行的运算很少
//T类的param1,U类的param2,V类的param3并不能相加然后return,因为有可能是string+int+float这样的组合
//例如,我们不能用模型行为x2,不能访问浮点数的游戏对象字段
//目前,我们将其当作类对象进行处理,这是基类,所有C#类隐式地从基类继承而来
//如何才能执行更多运算呢?
//为了解类型的一些特征,我们必须限制可能的类型,方法是对泛型参数施加限制
//为了给函数添加限制,我们在参数之后,函数主体之前输入where,后面跟我们将限制的泛型类型,即本例中使用的T,然后输入冒号
//在冒号后面,用逗号分隔具体的限制
//限制通常分为四个类别,可以用关键字Class来确保T是引用类型 where T : class
//或者使用关键字struct确保它是值类型 where T : struct
//可以使用关键字new后跟一对圆括号new()来确保它具有不含参数的公共构造函数 where T : new()
//可以使用类名称来表示T就代表这个类,或者通过多态表示T代表从中衍生的任意类 where T :Monobehaviour
//最后一个限制类别是接口,可以用接口名称来表示T已经实现这个接口 where T : IEnumerable
//为使用泛型方法,必须指定希望它使用的具体类型
public T LimitedGenericMethod<T>(T param) where T : struct
{
return param;
}
}
public class 使用泛型
{
void Start()
{
通用_泛型 myClass = new 通用_泛型();
//方法名称+尖括号+想要的类型+圆括号+任意参数
myClass.GenericMethod<int>(5);
//通过为类指定泛型类型,可以影响其中的字段、属性和方法的类型
}
}
//创建GenericClass是泛型的一种较为常见的用法
//有助于轻松实现数据结构
public class GenericClass<T> : MonoBehaviour
{
//这个类使用泛型类型T,这意味着在使用时,在类中用作类型的类型T的每个实例将替换为实际类型
T item;
public void UpdateItem(T newItem)
{
item = newItem;
}
}
//为了实例化这个类的对象,必须为T指定一个类型
//方法是输入类的名称,后面跟尖括号和所需类型
//在输入构造函数的名称以后,并在构造函数的参数列表之前也必须执行这个操作
public class GenericClassExample:MonoBehaviour
{
private void Start()
{
GenericClass<int> myClass = new GenericClass<int>();
//泛型最常见的一种用法是用于字典和列表等集合
myClass.UpdateItem(5);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 继承 : MonoBehaviour
{
//一个类继承另一个类时,会获得被继承类的特征
//如果ClassA拥有Dance和Sing两个方法,ClassB从ClassA继承而来,因此也会拥有Dance和Sing这两个方法
//无需在ClassB中创建这两个方法,因为
//它们已经存在于ClassA中
//公开的public的父类特征将存在于子类中,并且可供访问
//私有的private特征存在于子类中(注意,存在),但是不可访问
//保护的protected修饰符相当于public和private的混合,与公开的特征一样受保护的父类的所有特征将存在于子类中并且可供访问,但在父类和子类之外则不可访问,就像私有的特征一样
//Unity中的大多数类都是继承来的,作为组件应用于游戏对象的所有脚本的确都是从Monobehaviour类继承而来
//游戏对象、转换、start方法、update方法等项均来自Monobehaviour
//面向对象编程中的继承称为IS-A关系,这表示”子类是父类“,”爬行动物是脊椎动物“,”哺乳动物是动物“,”Capsule Collider是Collider
//在子类继承的项中,构造函数是一个例外,因为它们对类是唯一的,不会共享
//但是,在子类中调用构造函数时,其父类的构造函数会立即被调用
}
public class Fruit
{
Fruit()
{
}
}
//由于类可能有多个不同的构造函数,因此我们可能想要控制调用哪个基类的构造函数
//为此可以使用关键字base,通过在子类构造函数的参数列表后添加一个冒号,可以使用关键字base,在基构造函数的参数列表中显式调用基类的具体构造函数
/*
*public class Apple : Fruit
*{
* public Apple():base("apple")
* {
* }
* }
*/
//如果不显示调用基类的构造函数,则仍会隐式调用默认构造函数
//除了调用基类的构造函数,base关键字还可以用来访问基类的其他成员
//这种方法十分适用于访问基类版本的任何内容,因为它不同于派生的版本,覆盖函数时通常会有这样的需要
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///
/// 中文课堂观看地址:https://learn.u3d.cn/
///本课程提供中级脚本开发的基本知识。
///教程大纲 https://unity.cn/projects/intermediate-programming
///提问入口 https://unity.cn/ask/home
///
public class 多态 : MonoBehaviour
{
//多态是继承的一个特征,允许类拥有多个类型
//在继承层级结构中,任何子类都可以称为父类,这表示在需要基类的时候,可以用派生类来代替它
//如果想要创建一个包含所有Enemy对象的游戏集合,不必创建Orc和Goblin两个集合,而是创建一个集合,让它包含所有Enemy对象
//多态也适用于函数参数等
private void OnTriggerEnter(Collider other)
{
//Ontrigger函数通常包含Collider参数,通常被命名为other
//游戏对象没有Collider组件,但他们可能有Box Collider/Capsule Collider/Sphere Collider/Mesh Collider或者类似组件
//调用Ontrigger函数时,我们不知道会使用什么类型的Collider
//事实上,每个对象的特定Collider都会传入函数,由于所有这些不同的Collider。均继承自Collider父类,因此他们都将发挥作用
}
}
//需要注意的是,反过来则不成立。Orc是Enemy,但是Enemy不是Orc
//不能为需要子类的某个项,提供父类
//多态一种较为明智的做法涉及构造函数和对象引用
//可以声明基类类型的对象,然后调用其中一个派生类的构造函数
//ParentClass myClass=new ChildClass();
//这是因为变量引用需要的是基类的类型
//子类的构造函数会创建衍生类型的项,如果困惑,只需记得“子类是父类”就行
//因此,这种转换是有效的,整个过程被称为“向上转型”up-casting(拿一个子类对象当父类用)
//当对象向上转型时,他只能被视作其父类的一个对象
//myClass.ParentMethod();
//在本实例中,子类向上转型时,只能被视作父类
//这表示只能使用父类中可用的变量和方法
//在使用时,会把它们视作位于父类对象中
//虚函数是一个例外,虚函数将调用最新覆盖版本
//为了将这个子类视作子类,我们需要向下转型子类变量,使其恢复为子类类型
//具体方法是,将类型名称括在括号内,并将其至于变量前面
//(ChildClass)myClass
//我们可以在用一组括号括起来,并使用点运算符来访问成员
//((ChildClass)myClass).ChildMethod()
//也可以创建对这个新版本的引用
//ChildClass myChild=(ChildClass)myClass;
//myChild.ChildMethod();
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 成员隐藏 : MonoBehaviour
{
///通过继承,父类的成员在子类中自动可用或继承到子类中
//在子类中重新创建,即重新声明父类成员的过程被称为成员隐藏
//隐藏成员使用关键字new的方式略有不同
//为了隐藏基类的成员,应在成员的类型前面使用new声明子类成员
//一般情况下,这不会影响以这种方式声明的成员的使用
//但是当子类向上转型为父类和使用的成员时,它将是来自父类的成员,尽管实例为子类
//比如,Humanoid、Goblin、Orc类的敌人都将使用Humanoid类的Yell()方法,因为我们将Orc和Enemy对象声明为Humanoid,并且它们已经隐式向外转型为HUmanoid
//这种行为通常不是期望的行为,因此并不常用
//但这一点值得注意,事实上,这种行为与覆盖完全相反
}
public class ParentClass
{
int SampleValue = 0;
public void SayHello()
{
}
}
public class ChildClass:ParentClass
{
new int SampleValue = 5;
public new void SayHello()
{
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 覆盖 : MonoBehaviour
{
//覆盖override是指更改子类中的父类方法
//结果是,当我们调用方法时,将调用最新版本的方法或最新覆盖的版本
//使用继承层次结构时,我们通常想要使用与基类略微不同的函数版本
//这个操作非常简单,只需在子类中重新创建方,并且根据需要编写代码即可
}
public class Humanoid
{
public virtual void Yell()
{
//Raise hands to mouth
//Play "Yelling Sound"
}
}
public class Enemy: Humanoid
{
public override void Yell()
{
base.Yell();//让Enemy保留Humanoid的功能,同时添加自己的效果
//Attrack nearby enemies
}
}
public class Orc:Enemy
{
public override void Yell()
{
//Power up nearby orcs
}
}
//希望三类的对象调用Yell时有不同的效果,实际要做的是在每个子类当中覆盖Yell方法的父版本
//当我们尝试覆盖子类中的父方法时,Unity会发出警告,为了抑制该警告并且告知Unity我们就是要覆盖方法。
//可以用virtual和override关键字,这些关键字位于方法的返回类型之前
//父类中的方法定义为virtual,而所有子类中的方法定义为override
//声明为virtual的任何方法可被任何子类覆盖
//覆盖的另一种有趣的用法是,如果让每个子类为方法添加特定功能,同时不失去父类提供的原始功能
//为此,需要使用base关键字来同时调用方法的父版本
//例如,让Enemy保留Humanoid的功能,同时添加自己的效果
//覆盖对多态也非常有用,通过将父方法声明为virtual,将子方法声明为overridr。我们将有效覆盖方法的父版本
//结果是,当我们将子引用向上转型,为父对象,然后调用方法时,将调用整个方法的子版本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 接口 : MonoBehaviour
{
//接口可被视为关于功能的协定
//实现接口的任何类必须拥有其所有方法和属性
//作为交换,通过使用多态,其他类可将实现类视作接口
//需要注意的是接口不是类,不能有自己的实例
//继承是IS-A关系,即一个类继承自另一个类,而接口使用实现Implements关系,即一个类实现一个接口(也可以实现多个接口)
//接口通常在类外部声明,声明接口时,通常对每个接口使用一个脚本
//但在本示例中,我们将在同一个脚本中展示两个接口
//按照惯例,声明接口所使用的名称,以大写字母I开头,后跟以另一个大写字母开头的名称
//由于接口通常描述实现类将具备的某种功能,因此许多接口以后缀able结尾,但值得注意的是,这不是强制性的,并且可能有误导性,具体取决于接口
}
public interface IKillable
{
void Kill();//实现IKillable接口的任何类必须有一个与这个签名匹配的公共函数
}
public interface IDamageable<T>//这个接口具有泛型类T,这表示这个接口中的任意内容都可以具有泛型类型(没有也行)
{
//函数Damage需要一个类型为T的参数,当类实现具有泛型类型的接口时,必须选中这个类型,然后,必须始终使用相应类型
void Damage(T damageTaken);
void Test(int test);
}
//实现接口需要满足一些需求,也有一些好处
//为了实现接口,类必须公开声明这个接口中存在的所有方法、属性、事件和索引器
//如果不这样做,将导致错误
//因此,可以根据类实现的接口安全地对类的用途作出假设
//要实现接口只需在类具有的任何继承之后添加一个逗号,后跟接口的名称
//如果类不是从其他类继承而来,则不需要逗号
//如果接口具有泛型类型,则名称应后跟尖括号,并在里面输入类型
public class Avatar:MonoBehaviour,IKillable,IDamageable<float>
{
//请注意,函数主体与接口相互独立,可按希望的任何方式进行实现
//在游戏中如果想要实现全毁或者全灭效果,则这些接口来源可能很有用
public void Kill()
{
}
public void Damage(float damageTaken)
{
}
public void Test(int test)//所有的方法都要实现,类必须公开声明这个接口中存在的所有方法、属性、事件和索引器
{
;
}
}
//有继承为什么还要实现接口?——可以实现多个接口,但不能实现从多个类继承
//通过接口可以很好地提供广泛功能,更好的答案是,接口用于跨多个互不相关的类定义通用功能
//比如wall墙壁和car汽车,几乎没有什么共同点,唯一的共同之处是它们都是可破坏的
//因为这两者如此不同,所以继承父类毫无意义,但实现接口则非常实用
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 扩展方法 : MonoBehaviour
{
// 通过扩展方法,可以向类添加功能,而不必创建DriveType或者更改原始类型
//它们非常适用于需要向类添加功能,但不能编辑类的情况
//比如说,Unity内置的Transform类,我们无法访问其源代码
//假设我们想使用函数轻松充值Transform的位置、旋转和缩放
//这个函数理想的位置是放在Transform类中,但由于不能直接向这个类进行添加,并且将这个函数添加到派生类也没有任何意义
//所以我们将为其创建扩展
}
//扩展方法必须放在非泛型静态类中
//常见做法是专门创建一个类来包含他们
//(也就是说可以不同类的扩展方法都创建单独的类来专管专放,【扩展Transform】、【扩展Input】,
//也可以都放在一个类里面,通过第一个this参数区分是哪个类的扩展)
//扩展方法的用法与实例方法类似,它们也声明为静态方法
//要使函数成为扩展方法而非静态方法,需要在参数中使用this关键字
public static class ExtensionMethods//静态类
{
//扩展方法
public static void ResetTransformation(this Transform trans)//注意该方法声明为静态方法
{
//第一个参数包含this关键字,后面跟Transform类和任意参数名称
//第一个参数将是调用对象,因此当我们调用这个函数时,无需提供这个参数
//此外,这个第一个参数规定了这个方法属于哪个类
//如果我们想要更多参数,可以再次输入而不使用this关键字
//在方法中,现在我们可以编写代码来重置Transform
trans.position = Vector3.zero;
trans.localRotation = Quaternion.identity;
trans.localPosition = new Vector3(1, 1, 1);
//需要注意的是,尽管这个函数声明具有参数,但调用函数时,它将没有参数
//参数隐式地成为Transform的实例
}
public static void TestForMoreParameters(this Transform trans,int param1,int param2)
{
//param1,param2就是更多不使用this关键字的参数
}
}
//为了使用这个扩展方法,你只需将其视为所扩展的类的成员
public class UseExtensionMethod
{
Transform trans;
void Start()
{
trans.ResetTransformation();
//在本例中,我们扩展的是Transform
//可以认为这个方法现已成为Transform类的一部分,可以和其他Transform具有的属性和方法相同使用
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using SampleNamespace;
///
/// 命名空间测试
///
namespace SampleNamespace
{
public class 命名空间 : MonoBehaviour
{
//命名空间就像是类的容器,其目的是帮助组织脚本,避免脚本之间发生冲突
//比如在Unity中创建工具来帮助开发应用,可以将工具和实际应用放在不同的命名空间中
//这样自动补全功能不会建议过多不必要的类
//using关键字表示其后面的命名空间中的任何内容都可在脚本中使用
//如果在脚本顶部注释掉using命令。可以看到自动补全功能建议的可供使用的类大幅减少
//因为游戏对象、转换、刚体等许多类均位于UnityEngine命名空间中
//为了将我们的类放入命名空间中,需要用命名空间语法将类包围起来
//首先输入关键字namespace,然后是命名空间的名称,可以是现有的命名空间,也可以是新的
//在类的前后分别输入作用大括号,缩进表达归属关系
}
}
//有三种办法使用来自特定命名空间的类
//1.脚本顶部包含Using指令(目前看来这样最好)
//2.使用点运算符 无需在脚本顶部添加using SampleNamespace;
//每次要引用来自SampleNamespace的类时,都可以输入SampleNamespace.类
//这种方法可以避免歧义,但可能较为繁琐,尤其是对于长的大的命名空间名称,如SampleNamespace;
public class UseNameSpaceClass
{
void Start()
{
SampleNamespace.命名空间 myClass = new 命名空间();
}
}
//3.将编写的类放入需要访问的命名空间中,一般不建议使用这种办法,除非打算将这个类放入同一个命名空间中
//只要类位于不同的命名空间中,它们是可以使用相同名称的
//但是由于脚本的名称与其中包含的类的名称相同,因此脚本必须位于不同文件夹中,这样才能具有相同的类名(同一个文件夹目录下面不能重名))
//使用命名空间时要注意避免模糊定义
//例如System和UnityEngine这俩常用命名空间均包含Random类的定义,如果同时使用它们则需要通过点运算符来消除类的歧义
//是System.Random还是UnityEngine,Random
//第二个默认的Using指令是System.Collections命名空间,这表示命名空间可嵌套
//要嵌套命名空间,只需将一个命名空间声明括在另一个命名空间声明内即可
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class 列表和字典 : MonoBehaviour
{
// 两个泛型集合:列表和字典,二者的工作原理类似于数组,但是有一些明显区别
//列表像是大小动态变化的数组,不需提前知道列表将包含多少元素
private void InitList()
{
List<BadGuy> badGuys = new List<BadGuy>();//List是泛型类,因此在任何修饰符之后,我们输入类名,后跟要存储在列表中的类型
//然后为列表指定名称,由于列表是一个类,我们调用构造函数
//使用Add函数执行分配内容的操作,这将在链表末尾分配新元素
//Add函数的参数是要添加到列表的对象。在本例中,我们将为BadGuy调用构造函数
badGuys.Add(new BadGuy("Harvey", 50));
badGuys.Add(new BadGuy("Magneto", 100));
badGuys.Add(new BadGuy("Pip", 5));
//要访问列表项,可以像数组一样使用索引进行访问
//列表还有一个Count属性,作用类似数组的Length属性
int x = badGuys.Count;
//列表的RemoveAt和Insert等函数用于进行手动排列
//RemoveAt用于从列表中移除给定索引处的元素,这个元素上方的所有元素会下移一位
//Insert需要一个索引和一个元素,将这个索引之后的所有元素上移一位
//列表的最强大函数之一是Sort,可用于按给定类型的任何变量,对这个类型的列表进行排序,依赖于类型来实现IComparable接口
badGuys.Sort();//调用排序函数并输出查看
foreach(BadGuy guy in badGuys)
{
print(guy.name + " " + guy.power);
}
//重新创建(填充?)列表并且移除所有元素
badGuys.Clear();
}
public void InitDictionary()
{
Dictionary<string, BadGuy> badguys = new Dictionary<string, BadGuy>();
//我们使用这个字典来存储可用于识别特定BadGuy的不同搜索词
BadGuy bg1 = new BadGuy("Harvey", 50);
BadGuy bg2 = new BadGuy("Magneto", 100);
//访问与键相关的值非常类似于发访问数组或列表的元素
//但是我们不使用索引,索引对字典没有内在含义
BadGuy magneto = badguys["mutant"];//通过键返回对应的BadGuy对象
//如果字典中不存在这个键,则会抛出异常
//因此,如果无法保证键存在,最好使用TryGetValue方法
//这个方法具有一个键类型的参数,和值类型的输出参数
BadGuy temp = null;
if (badguys.TryGetValue("birds", out temp))
{
//如果作为第一个参数传递的键存在,则返回true
}
else
{
//虽然这种从字典中返回值的方法更加安全,但比直接引用具体键速度略慢
//为提高效率,可以在方括号内使用键,但前提是,指定键确定位于字典中
}
}
}
//声明实现接口,方便用Sort
public class BadGuy : System.IComparable<BadGuy>
{
public string name;
public int power;
public BadGuy(string newName,int newPower)
{
name = newName;
power = newPower;
}
public int CompareTo(BadGuy other)
{
//如果从中调用这个方法的对象大于被视作参数的对象,则函数返回正数
//如果从中调用这个方法的对象小于被视作参数的对象,则函数返回负数
//如果二者相等,则返回0
//定义一个对象是否大于另一个对象由程序员决定(程序员来决定怎样界定【大】,不一定非得是数值大,也可能是生命力强,防御力高blabla)
//我们这个函数首先检查BadGuy是否存在,如果不存在,则我们定义为【较大】,函数返回正数
//否则,我们让函数返回两个BadGuy的幂值差
if(other==null)
{
return 1;
}
return power - other.power;
//因此,如果从中调用方法是BadGuy较大, 则返回正数
//注意,这个结果可以基于任何依据,接口只要求我们实现方法、
}
}
//字典的工作原理与列表类似,但它有两种类型,这表示每个元素组成一个键值对,有时简称为KVP(Key Value Pair)
//字典的预期用途也与列表不同。列表通常用于替代需要更多灵活性或功能的数组
//字典用作可通过一个键或者多个键访问的值的集合
//声明字典的过程与列表非常相似,添加命名空间,然后声明变量
//第一个类型是键,这是为了访问第二个类型而引用的类型,第二个类型是值
补充一段《Unity’2017从入门到精通》上面关于数组、列表和字典的代码
using System.Collections;
using System.Collections.Generic;//使用链表List必须添加此命名空间
using UnityEngine;
public class CSharpArray : MonoBehaviour
{
public int[] array1 = new int[5];//长度为5的一维整型数组
public int[,] array2;//定义一个二维数组
public void InitArray1()
{
//赋值操作
for(int i=0;i<array1.Length;i++)
{
array1[i] = i;
}
//遍历输出
foreach (int item in array1)
Debug.Log(item);
}
public void InitArray2()
{
//分配数组长度
array2 = new int[2, 3];
//赋值操作
for(int i=0;i<array2.GetLength(0);i++)//GetLength获取一个32位整数,该整数表示Array指定维中的元素数
{
for(int j=0;j<array2.GetLength(1);j++)
{
array2[i, j] = (i + 1) * (j + 1);
}
}
//遍历输出:
foreach (int item in array2)
Debug.Log(item);
}
}
public class CSList:MonoBehaviour
{
List<string> mList = new List<string>();//声明一个字符串链表
public void InitList()
{
mList.Add("张三");
mList.Add("王五");
mList.Insert(1,"李四");//在张三之后,王五之前插入一个元素
for(int i=0;i<mList.Count;i++)
{
Debug.Log(mList[i]);//遍历列表打印元素
}
mList.RemoveAt(2);//移除下标为2(王五)
if(mList.Contains("李四"))//判断链表里是否存在李四
{
Debug.Log("链表里有李四");
}
mList.Clear();//清空链表
Debug.Log("链表长度:"+mList.Count);
}
public void TransFromListToArray()
{
//List可以与数组相互转换
//string-->List:
string[] str = { "1", "2" };//需要加个Static,否则报错
List<string> list = new List<string>(str);
//List-->string
List<string> list2 = new List<string>();
string[] str2 = list2.ToArray();
}
}
public class CSDictionary : MonoBehaviour
{
//基本用法
public void InitDictionary()
{
//创建一个字典对象,Key的类型是string,Value的类型是int
Dictionary<string, int> dic = new Dictionary<string, int>();
//用字符串找数字
//Add方法添加键值对
dic.Add("Jack", 13);
dic.Add("Tom", 18);
//从字典中移除键值对
dic.Remove("Jack");
//清空当前字典
dic.Clear();
//获取当前字典中Key、Value的个数
int count = dic.Count;
Debug.Log("当前字典中有" + count + "个KeyValue");
//检测字典中是否包含指定Key
bool b = dic.ContainsKey("Andy");
//检测字典中是否包含指定的Value
bool c = dic.ContainsValue(15);
//尝试获取指定的Key对应的Value
int s;
dic.TryGetValue("Andy", out s);
//如果当前字典包含Andy这个Key,那么获取对应Value并且保存在s中,b=true
//如果当前这个字典不包含Andy这个Key,那么s=null,b=false
dic.Add("Jack", 16);
//通过key获取Value
int age = dic["Jack"];
Debug.Log(age);
}
public void OverViewDictionary()
{
//遍历字典
Dictionary<string, int> dic = new Dictionary<string, int>();
//Add方法用来添加键值对
dic.Add("Jack", 13);
dic.Add("Tom", 18);
//遍历字典取值
foreach(var item in dic)
{
Debug.Log(item.Key + ":" + item.Value);//注意类型写的var,因为键和值都可能是任意类型
}
//通过键的集合取
foreach(string key in dic.Keys)
{
Debug.Log(key + ":" + dic[key]);
}
//通过值的集合取
foreach (int val in dic.Values)
{
Debug.Log(val);
}
//非要采用for的方法也可以
List<string> temp = new List<string>(dic.Keys);//字典先转换为链表
for(int i=0;i<temp.Count;i++)//遍历链表,但是取得Key以后还是靠字典输出Value(这不多此一举吗)
{
Debug.Log(temp[i] + ":" + dic[temp[i]]);
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 协程 : MonoBehaviour
{
//协同程序可被视为按时间间隔执行的函数
//这类函数与特殊的Yield语句搭配使用,yield语句从函数中返回代码执行
//然后,当函数继续时,将从上次停止的地方开始执行
}
public class CoroutinesExample : MonoBehaviour
{
public float smoothing = 1f;
public Transform target;//目标位置
private void Start()
{
StartCoroutine(MyCoroutine(target));//调用协同程序,以协同程序调用或者协同程序名称字符串为参数(可以写成传参的函数,可以写成""的字符串形式)
//这里展示的是第一种调用,这种办法更明智,但如果使用名称字符串进行调用,则还可以调用StopCproutine来提前终止
}
IEnumerator MyCoroutine(Transform target)//函数可以实现Ienumerator接口的任意内容
{
while(Vector3.Distance(transform.position,target.position)>0.05f)
{
//while循环会一直执行,直到对象和目标之间的距离小于0.05为止
//首先计算对象和目标位置之间的线性插值
transform.position = Vector3.Lerp(transform.position, target.position,smoothing*Time.deltaTime);
yield return null;//允许协同程序照常工作
//在执行这行代码时,将产生函数执行,并返回空的IEnumerator
//代码将在返回值指示的时间从这个点继续执行,由于返回值为null,这表示协同程序将在下一个Update后继续
//由于协同程序将在循环结束时继续,因此将重新评估循环条件
//如果Transform尚未达到目标,则进行插值计算、向目标靠近,协同程序再次yield,直到下个update
}
//当循环条件不再求值为true时,循环将退出,协同程序将在控制台上输出“Reached the target"
print("Reached the target");
//这时协同程序将再次yield,这次返回WaitForScconds类的实例
//构造函数使用new关键字调用,在本例中,传入3作为参数
yield return new WaitForSeconds(3f);
//在给定秒数之后,代码将继续执行,3秒以后MyCoroutine is now finished输出到控制台
print("MyCoroutine is now finished");
//到达协同程序末尾,执行结束
//while内使用yield实现了每一次update判断一下有没有到目标位置,如果没有就每帧插值往前移一点,移到了就输出第一句话,3s以后输出第二句话
//注意,我们创建了移动,但没有使用Update。也没有创建任何计时器,这个方法提高了代码的效率
}
//然而,协同程序真正的优势是与属性结合使用时发挥的作用
}
///
/// 这个脚本挂在移动的机器人上
///
public class PropertiesAndCoroutines:MonoBehaviour
{
public float smoothing = 7f;
private Vector3 target;
public Vector3 Target//公开属性Target用于封装目标字段target
{
get { return target; }
set
{
target = value;//设置目标字段
StopCoroutine("Movement");//停止任何移动协同程序
//注意,StopCprountines仅适用于已通过字符串调用启动的协同程序
StartCoroutine("Movement", target);//另一种调用StartCoroutine的方法,将协同程序的名称作为字符串传递,然后传递参数
}
}
IEnumerator Movement(Vector3 target)
{
//移动协同程序与上例相似,计算立方体位置到给定目标的插值距离并且移动,直到二者接近
while (Vector3.Distance(transform.position, target) > 0.05f)
{
transform.position = Vector3.Lerp(transform.position, target, smoothing * Time.deltaTime);
yield return null;
}
}
//这样设置脚本的效果是获得一个移动的游戏对象。点哪往哪走,而不必使用逐帧轮询值的Update函数
//轮询值会降低我们写代码的效率,虽然有时必须要这么做,但我们应该尽量避免
}
///
/// 这个脚本与环境相关联
///
//此脚本将使用PropertiesAndCoroutines脚本的对象的目标设置为在环境中被点击的任意位置
public class ClickSetPosition:MonoBehaviour
{
public PropertiesAndCoroutines corotineScript;
private void OnMouseDown()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
Physics.Raycast(ray, out hit);
if(hit.collider.gameObject==gameObject)//射线点到的物体就是自己(环境本身)——可以行走
{
Vector3 newTarget = hit.point + new Vector3(0, 0.5f, 0);//存储新的移动目标位置
corotineScript.Target = newTarget;//新的目标位置赋给机器人身上的组件,注意看这是大写的Target,不是private的小写target
}
}
}
public class CoroutineExample :MonoBehaviour
{
IEnumerator Start()
{
Debug.Log("Start:" + Time.time);
yield return StartCoroutine(WaitAndDebug());//Start也可以作为协程,并且协程yield return的时候还可以开启另一个协程
Debug.Log("Done:" + Time.time);
}
IEnumerator WaitAndDebug()
{
yield return new WaitForSeconds(5);//等待5秒
Debug.Log("WaitAndDebug:" + Time.time);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 四元数 : MonoBehaviour
{
// 在Unity中,转换旋转存储为四元数,它们类似于向量,但是有四个组件x,y,z和w
//这些组件相互依赖,并且配合使用,从而定义对象可能需要的任何旋转
//需要特别注意的是,由于下x,y,z,w组件是配合使用的,因此不能仅调整个别组件
//Unity有许多内置函数可以简化四元数的管理
//有一种管理旋转的系统叫做欧拉角,比如Inspector中显示的旋转值(四元数转换为欧拉角显示的),因为它更容易理解
//欧拉角旋转基于x,y和z轴的旋转,要遵从万向节锁,而万向节锁会妨碍增量旋转正常工作
//四元数不受万向节锁的影响
}
//这个功能可实现NPC在距离角色一定范围内时始终注视角色
//无论如何移动球体,角色始终面向球体
public class LookAtScript:MonoBehaviour
{
public Transform target;
private void Update()
{
//挂这个脚本的机器人相当于太阳,看着挂MotionScript的小球转
Vector3 relativePos = target.position - transform.position;//通过计算对象和目标之间的相对向量,可以使对象的z轴指向目标
transform.rotation = Quaternion.LookRotation(relativePos);//返回四元数旋转,旋转对象,使其面向目标
//其工作原理与transform.LookAt类似,但利用四元数明确设置旋转
transform.rotation = Quaternion.LookRotation(relativePos,new Vector3(0,1,0));//可以向函数传递第二个Vector3,用来告知函数哪个方向被认为是向上
}
}
public class MotionScript:MonoBehaviour
{
public float speed = 3f;
private void Update()
{
//仅根据水平输入轴横向移动对象
transform.Translate(-Input.GetAxis("Horizontal") * speed * Time.deltaTime, 0, 0);
}
}
//Slerp函数:球形插值,类似Lerp函数
//lerp线性插值,二者的最大区别是,lerp在两个四元数之间均匀插值,而Slerp在曲线上插值
//结果是,随着时间的推移,Lerp将提供均匀的变化,而Slerp开始后会变慢,并在中间时加快速度
//利用Slerp和一些前向移动来实现重力轨道效应
public class GravityScript:MonoBehaviour
{
//设置类似之前的LookAtScript
public Transform target;
private void Update()
{
//计算对象和目标之间的相对向量,但是这一次添加了偏移,将球体高度考虑进去(轨道不是伊贴地转,而是离地一定距离)
Vector3 relativePos = (target.position + new Vector3(0, .5f, 0) - transform.position);
Quaternion rotation = Quaternion.LookRotation(relativePos);//不将LookRotation储存在对象转换的旋转中,而是存储在名为rotation的四元数变量中
Quaternion current = transform.localRotation;//储存对象四元数变量的局部旋转
transform.localRotation = Quaternion.Slerp(current, rotation, Time.deltaTime);//使用Slerp函数缓慢转动对象,使其面向目标
//关键:旋转不会立即发生,而是随时间缓慢旋转
transform.Translate(0, 0, 3 * Time.deltaTime);//朝着目标稍微转动对象后,我们将其向前移动一点(如果没有这一行,小球只会看着机器人,但不会动)
//效果:球体始终向前移动,并转动以面向目标,这样就会呈现出流畅的轨道效应
}
}
//四元数类的identity属性
public class IdentityQuaternion
{
public Transform transform;
private void Start()
{
transform.rotation = Quaternion.identity;//将四元数设置为Quaternion.identity实际会将其欧拉旋转设为(0,0,0 )或无旋转
}
//四元数是处理旋转的最佳方式,绝对不能单独更改个别组件,总有一些功能可供使用
}
补充Unity官方的C#初级教程的LookAt,用于让游戏对象看向另一个transform
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class LookAt : MonoBehaviour
{
//LookAt可用于让游戏对象的正向指向世界中的另一个transform
//比如让镜头对准正在掉落的对象
public Transform target;//需要拖拽或者指定一个transform
private void Update()
{
transform.LookAt(target);//让对象看向target
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 委托 : MonoBehaviour
{
//通过委托,可以在脚本中创建可靠且复杂的行为
//委托可被简单地视作函数的容器,可以进行传递,或者像变量一样使用
//与变量一样,可以向委托分配值,并且这些值可以在运行时更改
//区别在于变量包含数据,而委托包含函数
}
public class DelegateScript:MonoBehaviour
{
//用delegate关键字创建委托,delegate关键字其后是委托的签名,与函数一样,委托有返回类型,名称,和参数列表
delegate void MyDelegate(int num);//明确委托模板,指示可以分配给委托哪些类型的方法(返回值void,一个int参数的)
MyDelegate myDelegate;//声明成员变量(类似创建一个类然后实例化一个对象
private void Start()
{
myDelegate = PrintNum;//方法赋给委托变量
myDelegate(50);//委托变量当作函数使用
myDelegate = DoubleNum;
myDelegate(50);
//我们可以用同一个委托变量调用两种不同的方法,这让我们更好地动态控制在游戏里调用哪些函数
}
//两个符合这个委托模板的(返回值为void,一个int参数)的方法
void PrintNum(int num)
{
print("print Num:" + num);
}
void DoubleNum(int num)
{
print("Double Nume" + num * 2);
}
}
//委托支持多播,允许单个委托变量同时代表多个方法
public class MulticastScript:MonoBehaviour
{
delegate void MultiDelegate();//委托模板,可以装(返回值void,无参数的函数)
MultiDelegate myMultiDelegate;//满足上面那个委托模板的具体委托变量(类似类的实例化对象)
private void Start()
{
myMultiDelegate += PowerUp;
myMultiDelegate += TurnRed;
//使用+=符号将多个方法分配给同一个委托变量
myMultiDelegate();//委托变量当作函数进行调用,会把加入的两个方法都执行
//通过这种方式,我们可以叠加功能
//如果要从委托变量中移除方法,可以使用-=运算符结合方法名称进行删除
myMultiDelegate -= PowerUp;
//必须注意一点的是,如果在向委托变量分配之前(还没加入方法,没赋值就调用)尝试将其视为函数进行调用,这种做法将引发错误,应该尽量避免
//目前任何未分配到方法的委托变量的值将为null,因此,在使用之前最好经常检查以确保委托不等于null
if(myMultiDelegate!=null)
{
myMultiDelegate();
}
}
//满足委托类型的两个函数
void PowerUp()
{
print("Orb is Powering up!");
}
void TurnRed()
{
GetComponent<Renderer>().material.color = Color.red;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 特性 : MonoBehaviour
{
//通过特性,可以在声明方法、变量或类时为其附加信息
//要获取并增强现有代码,或通过某种方式更改代码,这非常有用
//特性本身的作用和用途相差很大
}
public class SpinScript:MonoBehaviour
{
//特性直接编写在要修改的代码之上或者之前,一般不会影响脚本的任何其他部分
//所有特性的语法均以左方括号开始,然后右方括号结束特性,如果要传参,用()
//Inspector面板显示的不再是之前的速度属性,而是一个可以从最小值滑到最大值的滑块
[Range(-100,100)]
public int speed = 0;
//在本例中,我们将特性置于变量声明上方,我们也可以将其置于变量声明之前
//[Range(-100, 100)] public int speed = 0;这样也对
private void Update()
{
transform.Rotate(new Vector3(0, speed * Time.deltaTime, 0));//基于当前速度围绕y轴旋转对象
//这样一来,为speed变量提供所需的任意值,对象的旋转速度也会因此更改
//如果想要限制speed变量的取值范围,(方法1)除了编写代码来检测当前值,防止其超出范围,(方法2)就可以为其附加特性
}
}
//ExecuteInEditMode特性
//这个特性会使关联它的脚本运行,即使场景并没有处于运行模式也会执行脚本内容
//在类名上方或者前面输入左方括号,我们将其置于类名前面,因为ExecuteInEditMode特性将应用于脚本中的所有代码,而不只是一个部分
[ExecuteInEditMode]
public class ColorScript:MonoBehaviour
{
private void Start()
{
GetComponent<Renderer>().sharedMaterial.color = Color.red;//共享材质颜色设置为红色
}
}
//如果我们想要在不运行场景的情况下在Unity中看到这个变化,我们可对其应用ExecuteInEditMode特性
//效果:即使从未运行场景,脚本也已执行,将小球的颜色改为红色
//但是需要谨慎使用这一特性, 脚本通常在运行场景时运行,当停止运行场景时,对场景中游戏对象进行的任何更改都会被撤销
//但是,在编辑模式下执行的脚本将能够修改、创建和删除场景中的对象
//由于更改不是在运行模式下发生的,因此不会恢复,而是永久性的
//比如挂上这个脚本的场景里面的求踢会被永久改为红色,如果将另一个球体也拖入到场景中,他也将变成红色,因为球体素材已经改变
//要想解决这个问题,必须手动更改颜色
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 事件 : MonoBehaviour
{
//事件是一种特殊的委托,非常适用于想要提醒其他类发生了某个事件
//事实上,事件函数和公共多播委托非常相似
//事件可被视为广播系统,对事件感兴趣的任何类都可以将方法订阅到事件
//类似于事件是一个UP主,对他的内容感兴趣的所有人都可以关注和订阅他
//并且他发一个新视频新动态,这些关注他的人就会有一键三连、取关、分享等各自不同的动作
//发生这个特定情况,如点击按钮、充能或者玩家受伤,我们会调用事件,进而调用已订阅类的方法(牵一发而动全身)
}
public class EventManager:MonoBehaviour
{
public delegate void ClickAction();//创建委托类型(没有参数,返回值void)
public static event ClickAction OnClicked;//创建事件变量,名为“点击按钮”的事件,负责在发生相应情况时调用事件
//这也是一个静态变量,因此可以在类外使用,无需实例化这个类的对象(相当于一个面向全局的广播系统)
private void OnGUI()
{
if(GUI.Button(new Rect(Screen.width/2-50,5,100,30),"Click"))//在屏幕上创建一个按钮
{
if(OnClicked!=null)//与委托一样,如果我们调用没有订阅者的事件,将发生错误,所以也要判断事件是否为null
{
OnClicked();//玩家点击按钮时,像使用函数一样使用事件变量,从而使用我们的事件
}
}
}
}
//两个不同功能的脚本将充当事件的订阅者
public class TeleportScript:MonoBehaviour
{
private void OnEnable()//物体创建/启用/激活的时候订阅事件
{
EventManager.OnClicked += Teleport;//"点击按钮"这个事情一发生,就执行Teleport这个方法
}
private void OnDisable()//物体禁用/销毁/未激活时取消订阅
{
EventManager.OnClicked -= Teleport;//退订方法,确保事件发生时不会再调用我们的方法,这一步非常重要,如果不执行,可能会导致内存泄漏和游戏出错
}
//最好每次将方法订阅到事件时,必须同时设置相应的退订
//就按这个OnEnable、OnDisabled格式都可以,一般都是启用的时候关注订阅,不启用了就退订不再理会事件
void Teleport()//在Y轴方向一定范围内随机上下移动对象
{
Vector3 pos = transform.position;
pos.y = Random.Range(.3f, 1.0f);
transform.position = pos;
}
}
//和上面的类结构类似
public class TurnColorScript:MonoBehaviour
{
private void OnEnable()
{
EventManager.OnClicked += TurnColor;
}
private void OnDisable()
{
EventManager.OnClicked -= TurnColor;
}
//再次提醒,必须注意的是,要正确使用事件,并防止代码出错,从事件退订方法至关重要
void TurnColor()//随机改变对象材质颜色
{
Color col = new Color(Random.value, Random.value, Random.value);
GetComponent<Renderer>().material.color = col;
}
}
//多次点击按钮,每次都将调用订阅的方法
//我们可以看到,EventManager只需留意事件本身和事件触发器
//它不需要了解订阅它的Teleport和TurnColor方法(UP主只管产出内容,不用了解关注自己的粉丝会采取什么行为)
//Teleport和TurnColor也不需要相互了解(不同的粉丝之间也不需要相互了解)
//这样就是一个非常可靠而且灵活的广播系统
//为什么使用静态事件变量而非公开委托变量?
//事实上可以使用公开委托变量实现完全相同的事件功能
//事件只是特殊的委托,这种情况我们使用事件而非公开委托变量的原因是:
//事件具有内在安全性,而委托变量没有
//通过事件,其他类只能订阅和退订, 如果改用公开委托变量,
//其他类可能会调用或覆盖委托变量来执行各种不合理操作
//一般来说,如果想要创建一个包含多个类的动态方法系统(面向所有类的广播系统,面向所有受众的b站UP
//请使用事件变量,而非委托变量
完整代码会打包上传CSDN平台