一.c#基础 [Unity_Learn_RPG_1]
一.C#、unity C# 基础
1.面向对象
1).面向过程和面向对象
a.面向过程
就是把一个需求/问题,分成一步步的逻辑,很像数学里的解答。
关心的是解决问题的步骤。
b.面向对象
把需求分为一个个对象。
关心的是对象在做什么。以及对象之间的交互。
2).类和对象
a.先有类还是现有对象
设计角度:现有对象,再有类。根据需求,我们从中分析,提取出一有相同行为的"类”。
编码角度:肯定是先有类了,不然new 什么呢?
b.类与类
类是抽象的。是抽象的"类别"。类与类行为不同。
什么是行为?我的理解其实就是代码,如果一个类的代码改变了,那么它的逻辑也改变,逻辑改变体现在对象的实际执行上,也就是对象的行为也会因为逻辑的改变而改变。
那么不同类的代码肯定是不一样的,所以行为也不一样。
c.对象与对象
对象和对象数据不同。
不同类的对象就不说了。同类型的对象呢?
d.区分是对象不同还是类不同
比如人这个类。
游戏的捏脸功能就是如此,修改一些不同的属性数据即可。比如有的人腿长,有的人腿短,只是数据不同。
但是如果需求要人能飞,能射出蛛丝,“一般"来说我们就得新建一个类了。给它加上 飞行/喷射蛛丝 的行为。
所以,对象和对象的不同,只是数据上,比如腿的长短。而类就得是 行为/功能 上的不同。
- 类与类行为不同;
- 对象与对象数据不同。
2.主要思想
-
分而治之
将一个大的需求分解为许多类,每个类处理一个独立的模块。
拆分好处:独立模块便于分工,每个模块便于复用,可扩展性强。 -
封装变化
变化的地方独立封装,避免影响其他模块。 -
高内聚
类中各个方法都在完成一项任务(单一职责的类)。
复杂的实现封装在内部,对外提供简单的调用。 -
低耦合
类与类的关联性依赖度要低(每个类独立)。
让一个模块的改变,尽少影响其他模块。
1).分而治之和封装变化
分而治之其实就是模块化。
封装变化:
比如我们可以攻击,但是攻击可以分为用手,用武器,等。那么我们就应该用一个专门的武器类来封装这些。
难点在于,什么时候该用类来封装。老师的说法是,变化。
就比如攻击,当攻击方式多样化的时候,就应该封装了。
2).高内聚和低耦合
也很简单。老师也列了一个例子。
[例如:硬件高度集成化,又要可插拔]
最高的内聚莫过于类中仅包含1个方法,将会导致高内聚高耦合。
最低的耦合莫过于类中包含所有方法,将会导致低耦合低内聚。
-
高内聚高耦合
说的是,如果类都是最简单的一个方法那种,是高内聚了,但是实现一个功能,往往不可能这么简单,那么就得调用多个类,导致高耦合。 -
低耦合低内聚
极端的一个类囊括所有功能。低耦合的确,但是各种功能都在一个类,导致了低内聚。
2.继承
复习一些基础知识
1).栈和堆
推荐看这篇知乎回答。
-
管理方式:栈由编译器自动管理,无需人为控制。而堆释放工作由程序员控制,容易产生内存泄漏(memory leak)。
-
空间大小:在32位系统下,堆内存可以达到4G的空间(虚拟内存的大小,有面试官问过),从这个角度来看堆内存大小可以很大。但对于栈来说,一般都是有一定的空间大小的。
-
碎片问题:堆频繁new/delete会造成内存空间的不连续,造成大量的碎片,使程序效率降低(如何解决?如内存池、伙伴系统等)。对栈来说不会存在这个问题,因为栈是先进后出,不可能有一个内存块从栈中间弹出。在该块弹出之前,在它上面的(后进的栈内容)已经被弹出。
-
生长方向:堆生长(扩展)方向是向上的,也就是向着内存地址增加的方向;栈生长(扩展)方向是向下的,是向着内存地址减小的方向增长, 可以看第一张图。
-
分配方式:堆都是动态分配的,没有静态分配的堆。而栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,如局部变量分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手工实现。
-
效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持(有专门的寄存器存放栈的地址,压栈出栈都有专门的机器指令执行),这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的(可以了解侯捷老师的内存管理的视频,关于malloc/realloc/free函数等)。例如分配一块内存,堆会按照一定的算法,在堆内存中搜索可用的足够大小的空间,如果没有(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。总之,堆的效率比栈要低得多。
以上来自链接的文章中
2).实际编写
这是老师列出来的图。左侧是栈,右侧是堆。
- 方法是每个对象共享的,不存储在堆里。New只是在堆里申请空间,存储属性之类的数据成员。
所以说,对象和对象不同是数据不同。
3.抽象类
- 开闭原则:对扩展开发,对修改关闭
- 依赖倒置:依赖父级,不要依赖子级。父级的作用在隔离子级,隔离子级的变化。
1).语法
- abstract
- 可以有方法和属性
- 不能创建对象(因为是抽象概念,抽象概念怎么会有实体呢?)
2).语义
只表示做什么,有什么数据,不做具体处理。
3).适用性
- 不了解具体代码的实现
- 不希望创建实体类
4).抽象类的方法
- 类中只声明方法,不实现
- 描述做什么,不描述怎么做;
- 子类必须实现;
- 子类重写必须使用override。
5).virutal
- 子类选择实现
- 本类可以写逻辑代码
4.类的关系
两张图,一个需求变化。
原来是每个员工都需要计算薪资,所以基类是Employee。
后来有需求,可以改变一个员工的职位。
所以,Employee单独作为“员工”独立出来,而每个职位“Programer”,“Tester”,作为员工的职位,继承于基类Job。
- 找到变化点
1.类的四大关系
-
泛化:
子类与父类的关系,概念的复用,耦合度最高。因为父类以上,只要改变,子类也全会变。
B类泛化A类,意味B类是A类的一种;
做法:B类继承A类 -
实现:
抽象行为的具体实现,两者体现功能的关系,变化只影响行为;
A类实现B类,意味A类必须具体实现B类中所有抽象成员。
做法:实现抽象类、接口中的抽象成员。 -
关联(聚合/组合):
部分与整体的关系,功能的复用,变化影响一个类;
A与B关联,意味着B是A的一部分;
做法:在A类中包含B类型成员。 -
依赖:
合作关系,一种相对松散的协作,变化影响一个方法;
A类依赖B类,意味A类的某些功能靠B类实现;
做法:B类型作为A类中方法的参数,并不是A的成员。
如何判断两种类的耦合度,从上到下依次减少
- 继承:整个之后的家族都会增加(属性+方法);
- 聚合:同上,但是只是家族里增加了一个属性;
- 依赖:只是一个方法中用到;
2.总结:设计的八大原则(其四)
-
开闭原则(目标、总的指导思想)
Open Closed Principle
对扩展开放,对修改关闭。
增加新功能,不改变原有代码。 -
类的单一职责(一个类的定义)
Single Responsibility Principle
一个类有且只有一个改变它的原因。
适用于基础类,不适用基于基础类构建复杂的聚合类。 -
依赖倒置(依赖抽象)
Dependency Inversion Principle
客户端代码(调用的类)尽量依赖(使用)抽象的组件。
抽象的是稳定的。实现是多变的。
比如出行,出现是抽象的,但是实际上可以开车,走路等等 -
组合复用原则(复用的最佳实践)
Composite Reuse Principle
如果仅仅为了代码复用优先选择组合复用,而非继承复用。
组合的耦合性相对继承低。耦合低了,自然以后修改,增加也更容易了。 -
里氏替换(继承后的重写,指导继承的设计)
Liskov Substitution Principle
父类出现的地方可以被子类替换,在替换后依然保持原功能。
子类要拥有父类的所有功能。
子类在重写父类方法时,尽量选择扩展重写,防止改变了功能。// 调用 base.xxx 就是所谓的 “拓展“重写 // 尽量,不是一定 public void xxx(){ base.xxx(); .... }
-
接口隔离(功能拆分)
Interface Segregation Principle
尽量定义小而精的接口interface,少定义大而全的接口。本质与单一职责相同。
小接口之间功能隔离,实现类需要多个功能时可以选择多实现.或接口之间做继承。
例子:IPointerClickHandler -
IPointerClickHandler UI点击的调用。
-
IPointerUpHandler UI点击抬起时的调用。
-
IPointerDownHandler UI点击按下时的调用。
那为什么不用一个 IPointerHandler 来处理三个呢?就是这个原则了,尽量小而精,而非大而全。
-
面向接口编程而非面向实现(切换、并行开发)
客户端通过一系列抽象操作实例,而无需关注具体类型。
便于灵活切换一系列功能。
实现软件的并行开发。
排序的例子。 -
迪米特法则(类与类交互的原则)
Law of Demeter
不要和陌生人说话。
类与类交互时,在满足功能要求的基础上,传递的数据量越少越好。 因为这样可能降低耦合度。
5.多态
定义
- 父类同一种动作或者行为(父类型的引用调用同一方法),在不同的子类上有不同的实现。
- 继承将相关概念的共性进行抽象,并提供了一种复用的方式;
- 多态在共性的基础上,体现类型及行为的个性化,即一个行为有多个不同的实现。
实现手段
- 虚方法: 父类型的引用 指向 子类的对象,调用虚方法,执行子类中的重写方法。
- 抽象方法:抽象类的引用 指向 实现类的对象,调用抽象方法,执行实现类中重写方法。
- 接口:接口的引用 指向 实现类的对象,调用接口方法,执行实现类中实现方法。
1).方法重写
语法:在子类中使用override关键字修饰的方法。
作用:父类的方法在子类中不适用(虚方法),或父类没有实现(抽象方法)。子类重写可以满足对该方法的不同需求。方法重写时必须在方法前加override关键字。
a.三种方法可以重写:
- abstract 方法在子类必须重写,除非子类也是抽象类。
- virtual 方法在子类可以重写,父类方法的做法与子类不同。
- override 方法,已经重写过的方法,在子类还可以继续重写,除非被标识为sealed。(多重继承)
b.重写原理
- 子类在方法表中修改对应的地址;
- 修改父级方法表地址。
不管通过父类还是子类型的引用,调用方法时,都执行对象真实类型中定义的方法。
c.重写原理看图理解
如图。每个类,在内存的堆中,都有一个方法表。
-
重写的原理就是,实际运行时,把子级的方法表中的方法地址,覆盖掉父级方法地址,然后实际父类引用调用父类方法时,就会是子类的方法了。
-
因为这种覆盖是随着程序进行而会更改的,比如调用A子类,A子类先覆盖,然后调用。然后到B子类,就变成B子类覆盖,然后调用。
这种,在运行时修改的,也叫动态绑定。
d.对于继承逻辑的一些问题
重写方法的时候是否要调用父的逻辑?
我一直以来认为,既然继承了父类,那么我也有相同的方法,并且此方法应该包含父类的逻辑,这才算继承把?
目前老师的回答是。需要的时候就调用,不需要的时候就不调用。
如果是多重继承,但是我只需要上层的某个父类的逻辑,那我该如何调用?
比如,我只想调用某个父类的父类的相同方法,这应该如何调用?
f.重写
// (virtual/absctrac -> override)
// 重写。只是为了父类引用可以调用到子类的方法。
// 是否使用父类方法的逻辑,得用
base.xxxx();
2).方法隐藏
定义:在子类中使用new关键字修饰的与父类同签名的方法。
作用:父类的方法在子类中不适用,且通过子类型引用调用时,隐藏掉父类继承的旧方法,好像该方法不存在。
隐藏原理
子类在自己的方法表中增加一个新地址。
- 通过子类引用调用时使用新纪录(自己的),执行子类中新方法;
- 父类引用调用时使用旧纪录,执行父类中方法。
比如
C { call(); }
A extend C{ call(); }
B extend C{
virtual call();
}
toCall(C temp){
temp.call();
}
toCall(B) --> 用的是B类的Call,而不是C类的Call
toCall(A) --> 在toCall里,用的不是A类的call,而是C类的Call
想要使用A类的Call,要改成
toCall(C temp){
(temp as A).call();
}
所以说,如果是方法隐藏,那么“引用类型”是什么,那就只会调引用类型的方法,不会因为传入的是子类,就调用子类的方法。
虚方法
定义:用vritual关键修饰的已实现方法。
作用:可以在子类中重写的方法。
动态绑定(晚期绑定)与静态绑定(早期绑定)
-
绑定:类型与关联的方法的调用关系,通俗讲就是一个类型能够调用哪些方法。(内存中有张表)
-
静态绑定:是指调用关系是在运行之前确定的,即编译期间。
-
动态绑定:是指调用关系是在运行期间确定的。
动态绑定因为在运行期确定,占用运行时间,但是更灵活。
- 方法重写是动态绑定。每次调用的时候,子类要改父类的地址。因为不确定是哪个子类,的在运行的时候才能确定。
- 方法隐藏是静态绑定。调用时,不改父类的地址,所以是明确的一对一关系,直接编译期间确定就行。
最后
优先选择方法覆盖,因为是静态绑定,速度更快。
6.接口
1).定义
-
接口定义一组对外的行为规范,要求它的实现类必须遵循。
-
接口只关注行为,不关注数据,且不关注行为的实现,实现由实现类完成。
-
接口自身表达“能够做”,不表达“如何做”。
-
接口是一组行为的抽象,它的方法没有方法体,也就是自己不写方法的逻辑。
//一组:接口中可以包含多个方法; //对外:接口成员是要求子类实现,自己不要用。 //行为:接口中只能包含方法成员(属性、方法) //规范:要求子类必须自行实现
2).抽象类与接口的选择策略
-
抽象类与子类之间关系:is a [是一种]。内部的属性等,子类都可以直接用。
-
接口与实现类之间关系:can do [能够做(功能)]。内部的东西不可以直接用,必须再次实现。
-
接口与接口之间可继承,且可以多继承。
-
类与类是单继承,类与接口是多实现,接口与接口是多继承。
父类,只能有一个。而接口,可以很多个。
当都可以用时,优先选择接口。
3).作用
-
规范不同类型的行为,达到了不同类型在行为上是一致的。
比如,伤害。
玩家和敌人,肯定都能受到伤害,那么它们可以继承同个父类。
但是如果,树木,房子等也可以受到伤害,那就不能用同个父类了。 -
扩展一个已有类的行为。
或者在设计的后期,一个类以及设计并且实现完毕,这时候需要增加一个功能,一般也会用接口。
4).语法
- 使用interface关键定义。接口名建议用”I”开头,其后单词首字母大写。
- 接口中不能包含字段,可以包含:方法,属性,索引器,事件。
- 接口中的成员一定是public abstract的,但是不能写。
- 接口中的所有成员不能有实现,全部默认抽象的。
- 实现类实现接口用“:”与继承相同。
- 实现类实现可以实现多个接口,且每个接口中所有的成员必须都实现。
- 接口中的成员在实现类中以public的方式实现(除显式实现)。
- 接口的引用可以指向实现类的对象。
- 接口内的所有属性,方法等,它的访问级别跟 interface 是一致的
public interface IXXX 那就都是public的
internal interface IXXX 那就都是internal的
5).接口的显式实现
a.作用:
- 解决多接口实现时的二义性
- 解决接口中的成员对实现类不适用的问题
比如两个接口,都有同一个名称的方法。这时候如何实现,如何调用呢?
b.做法:
- 在实现的成员前加接口名,并且不能加任何访问修饰符,默认为private
- 显式实现成员只能通过接口类型的引用调用。
Void InterFace1.Fun()
{ }
c.使用场景
- 两个接口同个方法名,这时候用,这两个方法都会变成私有。只有父类引用才能调用的到(父类肯定不是私有啊);
- 这个方法,对于我这个类,并不需要实现且暴露给外部。也是变成了私有,外部调不到。
6).Framework常用接口
-
IComparable 可比较,使类型支持比大小的功能
如图,使用Array.Sort 进行排序时,Grenade需要实现 IComparable 接口的 CompareTo 方法才可以,不然会报错。也就是说,Array的排序功能,是通过 IComparable 接口,这个功能实现的。 -
IComparer 比较器,提供比较的方法,常用于排序比较
-
IEnumerable 可枚举,使类型支持简单迭代(foreach)
-
IEnumerator 枚举器,支持MoveNext ,自己可以控制迭代的节奏
a.何时使用 IComparer,何时使用 IComparable
-
IComparable
Array.Sort(array)
IComparable 的逻辑,是Sort里直接调用传入的类的CompareTo,写在类里。所以,如果这个类大部分都用这种方法排序,那么就使用 IComparable。
-
IComparer
Array.Sort(array, new XXXComparer);
IComparer 是需要在使用Sort时,new一个传入的,所以一般不会写在array代表的类里,它更灵活,有点像策略模式。但这样就说明它这个类的排序方式多变。
综上,这两个其实可以结合使用。常用的放类里,不常用/特殊的 放外面。
b.C#表达抽象的语义,有以下三种方法。
- 抽象类:一个概念的抽象(普通成员,抽象成员)
这就不多说了,已经可以作为一种“类”来表达了。 - 接口:一组行为的抽象(多种抽象成员)
一组行为,比如接口里的多个函数方法。接口常用于不能当作类,但是又具有部分共同功能的抽象。 - 委托:一类行为的抽象(同一种类多个方法)
委托,其实也就是函数回调。函数能又多少抽象逻辑呢?所以说是一类行为,可以传多个模板类(也不可能无限传模板类参数吧)。
由上往下,耦合度越低,抽象程度越低,功能也越简单。
7.协程
1.)迭代器
foreach(var item in hand) {
}
//foreach 的原理,就是如下的代码
// 1.获取迭代器
IEnumerator iter = hand.GetEnumerator();
// 2.移动到下一个元素
while(iter.MoveNext() ) {
// 3.获取元素
Console.WriteLine(iter.Current);
}
如上,foreach的原理如此。
public class Hand :IEnumerable{
public IThrowable[] AllObject { get; set; }
public IEnumerator GetEnumerator() {
return new HandEnumerator() {
Target = AllObject
};
}
public void Thorwing(IThrowable temp) {
temp.Fly();
}
}
// Hand的迭代器
public class HandEnumerator : IEnumerator {
public IThrowable[] Target { get; set; }
private int index = -1;
// 获取当前数据
public object Current{
get {
return Target[index];
}
}
public bool MoveNext() {
index++;
return index < Target.Length;
}
public void Reset() {
throw new NotImplementedException();
}
}
实现方式,就是类继承,IEnumerable,代表此类有这个功能。
子类实现一个迭代器类,继承 IEnumerator ,代表它是一个迭代器类,并且从父类传入需要迭代的“队列”,由此实现的接口代码才是实际的逻辑。父类也只是返回一个自己的迭代器类。
2).迭代器 -> 协程
我们重新实现一下 GetEnumerator 的逻辑
public class Hand :IEnumerable{
/*
* 传统代码
public IEnumerator GetEnumerator() {
return new HandEnumerator() {
Target = AllObject
};
}
*/
public IEnumerator GetEnumerator() {
/*
将 yield 以前的代码,分配到 MoveNext 方法中
将 return 后的数据,分配到 Current 中
*/
for(int i = 0; i < AllObject.Length; i++) {
yield return AllObject[i];// 返回数据, 退出方法
}
}
}
还是看之前,迭代器的实现,这里改成用 yield。
//foreach 的原理,就是如下的代码
// 1.获取迭代器
IEnumerator iter = hand.GetEnumerator();
// 2.移动到下一个元素
while(iter.MoveNext() ) {
// 3.获取元素
Console.WriteLine(iter.Current);
}
我们看实际foreach的逻辑代码,不同点在这段代码
IEnumerator iter = hand.GetEnumerator();
它并不会实际执行,而是在
while(iter.MoveNext() )
中才会跳到
for(int i = 0; i < AllObject.Length; i++) {
yield return AllObject[i];// 返回数据, 退出方法
}
里,做第一次循环执行。
所以说:
public IEnumerator GetEnumerator() {
/*
将 yield 前的代码,分配到 MoveNext 方法中
将 return 后的数据,分配到 Current 中
*/
for(int i = 0; i < AllObject.Length; i++) {
yield return AllObject[i];// 返回数据, 退出方法
}
}
3).Unity 协同程序(Coroutine)
a.定义
具有多个返回点(yield),可以在特定时机分部执行的函数。
b.原理
private IEnumerator iter;
private void OnGUI() {
if(GUILayout.Button("启动")) {
iter = Fun1();
}
if(GUILayout.Button("执行一次")) {
iter.MoveNext();
}
if(GUILayout.Button("携程")) {
//StartCoroutine(iter);
//每帧调用一次 MoveNext 方法
//相当于
StartCoroutine( Fun1() );
}
}
private IEnumerator Fun1() {
for(int i = 0;i < 5;i++) {
print(i + "--" + Time.frameCount);
//yield return null;
yield return new WaitForSeconds(1);
}
}
}
先附上所有代码
携程本质上是执行迭代器
private IEnumerator Fun1() {
for(int i = 0;i < 5;i++) {
print(i + "--" + Time.frameCount);
//yield return null;
yield return new WaitForSeconds(1);
}
}
可以看到,fun1返回的是 IEnumerator 对象,也就是迭代器对象
StartCoroutine( Fun1() );
同时,用迭代器的模板也是可以执行的
if(GUILayout.Button("启动")) {
iter = Fun1();
}
if(GUILayout.Button("执行一次")) {
iter.MoveNext();
}
唯一的不同在于, StartCoroutine 不需要我们手动点击按钮执行MoveNext(),而是由C#自动控制的。
控制携程迭代的间隔
private IEnumerator Fun1() {
for(int i = 0;i < 5;i++) {
print(i + "--" + Time.frameCount);
yield return null;
}
}
携程默认是每个渲染帧(Time.frameCount)调用一次
private IEnumerator Fun1() {
for(int i = 0;i < 5;i++) {
print(i + "--" + Time.frameCount);
//yield return null;
yield return new WaitForSeconds(1);
}
}
在 return 后增加一个 new WaitForSeconds(1); 那么,每次的间隔都会变成 1s,如图。
总结
- GameObject的某个脚本有协程,如果脚本enbaled了,那么协程仍会进行
- GameObject的某个脚本有协程,如果GameObjectenbaled了,那么协程会直接停止
Unity每帧处理GameObject中的协同程序(不是Component的协程),直到函数执行完毕。
流程
- 当一个协程函数启动时,本质创建迭代器对象;
- 调用MoveNext方法,执行到 yield 暂时退出;
- 待满足条件后再次调用MoveNext方法,执行后续代码,直至遇到下一个yield为止。
如此循环至整个函数结束。
c.语法
通过MonoBehaviour中的StartCoroutine启动,StopCoroutine停止。
协程函数返回值类型为IEnumerator,方法体中通过yield关键字定义返回点,通过return xx对象定义继续执行的条件。
可以被yield return 的对象:
- null 或者 数字 —> 等待一个渲染帧
- new WaitForSeconds(1) --> 等待指定时间
- new WaitForSecondsRealtime(1) --> 等待指定时间(不受时间缩放影响)
- new WaitForFixedUpdate() --> 等待一个物理帧
- new WaitForEndOfFrame() --> 等待一帧结束
- new WaitWhile( 委托 ) —> (下回分解……)
- Coroutine --> 在另一个协程函数执行完毕后再执行。
- WWW -->
d.作用
1.延时调用。
2.分解操作。
e.[例1]颜色变化
// 透明度变化
public float fadeSpeed;
public IEnumerator FadeOutTest() {
Color currentColor;
do {
currentColor = mt.color;
currentColor.a -= fadeSpeed * Time.deltaTime;
mt.color = currentColor;
yield return null;
} while(currentColor.a > 0);
currentColor.a = 0;
mt.color = currentColor;
}
// 颜色变化
public Color EndColor;
public AnimationCurve curve;
public IEnumerator FadeOut() {
Color oriColor = mt.color;
//for(float x = 0; x <= 1; x += Time.deltaTime) {
// 2s,变慢
for(float x = 0; x <= 1; x += Time.deltaTime/2) {
mt.color = Color.Lerp(oriColor, EndColor, curve.Evaluate(x) );
yield return null;
}
}
// 动画曲线:提供数值可视化的操作面板
// Color.Lerp 将数值的变化,变为颜色的变化
代码就不说了
-
AnimationCurve
动画曲线:提供数值可视化的操作面板。上面的颜色变化中,x的最大值是1,也就是1s,那么要如何延长时间呢?如下代码:
//for(float x = 0; x <= 1; x += Time.deltaTime) { // 2s,变慢 for(float x = 0; x <= 1; x += Time.deltaTime/2) {
-
Color.Lerp
Color.Lerp 将数值的变化,变为颜色的变化
nity3D中的线性插值Lerp()函数解析
f[例3]复杂协程的执行顺序
//a1 b1 d1 f1 ...(2s)... c106 e106
private Coroutine coroutine;
private void Start() {
print("a: " + Time.frameCount);
coroutine = StartCoroutine( Fun1() );
print("d: " + Time.frameCount);
StartCoroutine(Fun2());
print("f:" + Time.frameCount);
}
private IEnumerator Fun1() {
print("b:" + Time.frameCount);
yield return new WaitForSeconds(2);
print("c:" + Time.frameCount);
}
// 这里return ciroutine 是指等待 coroutine 指向的那个协程一次运行结束(下个yield之前/协程运行完)
private IEnumerator Fun2() {
yield return coroutine;
print("e:" + Time.frameCount);
}
这里的顺序是 a1 b1 d1 f1 …(2s)… c106 e106
a1,这里的1代表是哪一帧,所以可以得出结论,在yield之前的操作是不会放到下一帧的。
g.[例2]寻路
public Transform[] wayPoints;
private float moveSpeed;
public IEnumerator PathFinding() {
for(int i = 0; i < wayPoints.Length; i++) {
//移动到目标点
yield return StartCoroutine( MoveToTarget(wayPoints[i].position) );
}
}
private IEnumerator MoveToTarget(Vector3 position) {
transform.LookAt(position);
while(Vector3.Distance(transform.position, position) > 0.1f) {
transform.position = Vector3.MoveTowards(transform.position, position, moveSpeed * Time.deltaTime);
yield return new WaitForFixedUpdate();
}
}
private void OnGUI() {
if(GUILayout.Button("Go")) {
StartCoroutine( PathFinding() );
}
}
主要是讲解分解操作,利用了协程
yield return new xxxcoroutine;
等待xxx协程执行完毕再继续,的特性。来分解寻路到每个点的步骤。
8.抽象工厂
可访问性不一致
属性有public,private,protected等访问权限关键字。类也有
- 属性默认是private
- 而类,枚举、接口等默认是internal
如何设计
老师的说法是。这种设计模式,难度有些高,要在项目初期,由经验丰富的程序员来设计比较好。
因为这种模式的拓展并不是很好。
9.反射
基本用在框架级的代码
1).基础定义
a.定义
动态获取类型信息,动态创建对象,动态访问成员的过程。
b.作用
在编译时无法了解类型,在运行时获取类型信息,创建对象,访问成员。
c.流程
- 得到数据类型
- 动态创建对象
- 查看类型信息(了解本身信息,成员信息)
具体实现流程,请看 4.
2).常用类
a.取得数据类型Type
方式:
- Type.GetType(“类型全名”):适合于类型的名称已知。
- obj.GetType():适合于类型名未知, 类型未知,存在已有对象。
- typeof(类型):适合于已知类型。
- Assembly.Load(“XXX”).GetType(“名字”):适合于类型在另一个程序集中。 Untiy中目前基本不使用。
b.Type类常用Get系列方法 Is系列属性。
- MethodInfo(方法)
重要方法: Invoke - PropertyInfo(属性)
重要方法:SetValue GetValue - FieldInfo(字段)
重要方法:SetValue GetValue - ConstructInfo(构造方法)
重要方法:Invoke
3).动态创建对象
Activator.CreateInstance(string 程序集名称,string 类型全名)
Activator.CreateInstance(Type type);
Assembly assembly = Assembly.Load(程序集);
assembly.CreateInstance(Type);
//找到有参构造方法,动态调用构造方法
type.GetConstructor(typeof(string)).Invoke()
4).反射的具体行为
// 编译时
User user1 = new User();
user1.ID = 1001;
user1.LoginID = "zs";
user1.Print();
// 动态 ---> 运行时
// 获取 Type
// -- 根据字符串获取类型
Type type = Type.GetType("Day5.User");//命名空间.类名
// 目的:这样我们完全可以把 需要使用的类,放在表里,让使用的人来选择
//Type type = Type.GetType( Console.ReadLine() );
// -- 根据对象获取类型
//Type type = user1.GetType();
// -- 根据数据类型
// Type type = typeof(User);
// 创建对象
object instance = Activator.CreateInstance(type);
// 访问成员
PropertyInfo IDProperty = type.GetProperty("ID");
// 当你确定使用的这系列类,一定由这个 属性 的 类型 时
IDProperty.SetValue(instance, 1001);
// 当你不确定使用的这系列类,的某个 属性 的 类型 时
object idValue = Convert.ChangeType("1001", IDProperty.PropertyType);
IDProperty.SetValue(instance, idValue);
PropertyInfo LoginIDProperty = type.GetProperty("LoginID");
LoginIDProperty.SetValue(instance, "zs");
// 获取方法
MethodInfo printMethod = type.GetMethod("Print");
// 调用方法
printMethod.Invoke(instance, null);
这里上面的 User 代码,和 下面的一长串代码,最后得到的是一样的 User。
不同的是,上面的User是写代码的时候,就会知道的类型,而下面的 “反射” 写法,则是运行时才会知道具体是什么类。
a.按需使用类
// -- 根据字符串获取类型
Type type = Type.GetType("Day5.User");//命名空间.类名
// 目的:这样我们完全可以把 需要使用的类,放在表里,让使用的人来选择
//Type type = Type.GetType( Console.ReadLine() );
把上面和下面代码对比一下,就知道它的用法了。
比如,游戏中的buff。这时候,可以在表里填写这一类型的类名和参数,这样就可以灵活变化和测试,到底应该使用那些buff。
b.反射如何使用函数(方法)
// 获取方法
MethodInfo printMethod = type.GetMethod("Print");
// 调用方法。null代表无参数。object[] 是参数按要求
printMethod.Invoke(instance, null);
c.灵活使用类的属性类型
// 访问成员
PropertyInfo IDProperty = type.GetProperty("ID");
// 当你确定使用的这系列类,一定由这个 属性 的 类型 时
IDProperty.SetValue(instance, 1001);
// 当你不确定使用的这系列类,的某个 属性 的 类型 时
object idValue = Convert.ChangeType("1001", IDProperty.PropertyType);
IDProperty.SetValue(instance, idValue);
我们可以不知道属性的类型,如何做请看代码。
这里有个,不灵活的地方,就是属性名称。我们可以不知道属性的类型,但是名称是一定要知道的。
5).[例子]Json转换器
public static string Object2Json(object obj) {
// 获取所有属性(名称/值)
// 根据规则拼接字符串
// 提示:在MSDN中 搜索 "StringBuilder"类
Type type = obj.GetType();
PropertyInfo[] allProperty = type.GetProperties();
StringBuilder builder = new StringBuilder();
builder.Append("{");
foreach(var item in allProperty) {
builder.AppendFormat("\"{0}\":\"{1}\",", item.Name, item.GetValue(obj) );
}
builder.Remove(builder.Length - 1, 1);
builder.Append("}");
return builder.ToString();
}
public static T Json2Object<T>(string json) where T:new(){
// 创建对象
// 字符串解析(提取 属性名、名称值)
// 根据属性名 设置属性
// 提示:在 MSDN 中搜索"String"类
//Type type = typeof(T);
//object instance = Activator.CreateInstance(type);
T instance = new T();
Type type = instance.GetType();
json = json.Replace("\"", "").Replace("{", "").Replace("}", "");
//json.Replace("\"", string.Empty);
string[] keyValue = json.Split(':','c');
for(int i = 0;i < keyValue.Length - 1; i++) {
PropertyInfo property = type.GetProperty(keyValue[i]);
property.SetValue(instance, Convert.ChangeType(keyValue[i + 1], property.PropertyType) );
}
return instance;
}
反射目前都是通过 类型 去找属性的,而不是通过 对象。
a.Object2Json
builder.Append("{");
foreach(var item in allProperty) {
builder.AppendFormat("\"{0}\":\"{1}\",", item.Name, item.GetValue(obj) );
}
builder.Remove(builder.Length - 1, 1);
builder.Append("}");
主要就是这段。没啥好说的。
Q:如果是属性是某个类,该如何呢?
A:自己的想法是,把那个属性也传入 Object2Json(),插入返回的 json string 即可。
b.Json2Object
主要看这里
// 使用 replace 把所有 “ { } 都替换为 空字符串。
// 可以得到 只剩 “key:value,key:value,key:value” 这样的字符串
json = json.Replace("\"", "").Replace("{", "").Replace("}", "");
//json.Replace("\"", string.Empty);
// 按 :和 ,把 key 和 value 都单独出来,农场队列。
// 可以得到 。{key,value,key,value, ...} 这样的队列
string[] keyValue = json.Split(':','c');
6).反射与抽象工厂(8.中的例子)的互动
public static DaoFactory Instance {
//如果增加新的存储方式,违反开闭原则
//选择子类
get {
if(GameMain.Type == "Client") {
return new ClientDaoFactory();
} else {
return new ServerDaoFactory();
}
//
if(null == instance) {
// 动态(利用字符串)创建对象
// 定义规则:GameMain.Type + Factory
Type type = Type.GetType(GameMain.Type + "Factory");
//return Activator.CreateInstance(type) as DaoFactory;
instance = Activator.CreateInstance(type) as DaoFactory;
}
// www.......com/user/login? loginID&zs
// UserHandle
return instance;
}
}
这里用反射,重写了之前的抽象工厂方法。
老师的意思是说,这里在开发中,属于并行开发,我们不知道其他程序会写出哪些工厂,所以用反射来灵活的配置。
因为反射很耗时间,所以还用了单例的方法,并且列出了规则。
7).总结
- 反射的动态,就是用字符串(老师的说法)
- 无法确定用什么类,无法在代码中 new 出来的,就用发射
10.角色控制
1).需求分析
a.需求分析
- 首先分析需求,老师已经帮我们分析好了。如图。
- 1-4个红点即是我们要制作的类。
b.代码模板修改以及增加
如图,在编辑器的如图路径下,保存着Unity所有的模板。
这里的文件名称什么意思呢?
81-C# Script-NewBehaviourScripts.cs
- 81 这个数字,应该指的是排序/分类。
- c# Scrip 指的是我们在Unity右键后,显示的创建选项名称
- NewBehaviourScripts 指的是新建出来的东西默认名称。
如下图。
比如我们常用到的mono模板,可以看到和编辑器里看到的是不一样的。#SCRIPTNAME#是我们的类名称,要等创建的时候才会赋值过去。
2).制作
11.Unity的单例模板类
public class MonoSingleton<T> : MonoBehaviour where T:MonoSingleton<T>{
// T 表示子类类型
private static T instance;
public static T Instance {
get {
if(instance == null) {
instance = FindObjectOfType<T>();
if(instance == null) {
//创建脚本对象
instance = new GameObject("Singleton of " + typeof(T)).AddComponent<T>();
} else {
instance.init();
}
}
return instance;
}
}
protected void Awake() {
if(instance == null) {
instance = this as T;
init();
}
}
public void init() {
}
/*
* 备注:
* 1.适用性:场景中存在唯一的对象,即可让该对象继承当前类
* 2.如何适用:
* -- 继承时必须传递子类类型
* -- 在任意脚本生命周期中,通过子类类型访问Instance属性
*/
老师给了一个,Unity的单例基类,继承于它的,都可以使用单例模式。
一般我是把单例在开始的场景中直接挂上,然后dontdestory,就可以一直存在了。老师这种,如果没有挂上,则会动态在场景中创建对象。
a.泛型类
public class MonoSingleton< T >
这里的T和函数里的T都表示一个未知类,可以让传入,让整个系列的类,使用自己传入的类型来生成属性,以及使用。
b.约束
public class MonoSingleton<T> : MonoBehaviour where T:MonoSingleton<T>
where 后面就代表约束,具体语法我也不是很清楚,看msdn去。
where T:MonoSingleton<T>
这段表示的是,T是继承于MonoSingleton
约束告知编译器类型参数必须具备的功能。 在没有任何约束的情况下,类型参数可以是任何类型。
c.Unity中单例的获取
public static T Instance {
get {
if(instance == null) {
instance = FindObjectOfType<T>();
if(instance == null) {
//创建脚本对象
instance = new GameObject("Singleton of " + typeof(T)).AddComponent<T>();
} else {
instance.init();
}
}
return instance;
}
}
首先,肯定是判重拉,如果没有instance,得先在世界中查找
FindObjectOfType<T>();
如果还是没找到,那我们就要创建 UnityObject 然后 AddComponent< T >
//创建脚本对象
instance = new GameObject("Singleton of " + typeof(T)).AddComponent<T>();
d.不用Awake,改用Init
protected void Awake() {
if(instance == null) {
instance = this as T;
init();
}
}
public void init() {
}
这是老师的做法:
一般来说,只要生成一个 component 后,一定会执行 awake。而我们为保证在没有第一次调用 Instance 而走到Awake,就在 Awake 里也加了预防代码。
一般都用 Awake做初始化,现在改用了 Init 来做初始化。并且使用 protected 来限制它的访问级别。
其实暂时还不太理解init的这种写法,等以后实际使用时再慢慢消化吧。