一.c#基础 [Unity_Learn_RPG_1]

一.C#、unity C# 基础

1.面向对象

在这里插入图片描述

1).面向过程和面向对象

a.面向过程

就是把一个需求/问题,分成一步步的逻辑,很像数学里的解答。
关心的是解决问题的步骤。

b.面向对象

把需求分为一个个对象。
关心的是对象在做什么。以及对象之间的交互。

2).类和对象

a.先有类还是现有对象

设计角度:现有对象,再有类。根据需求,我们从中分析,提取出一有相同行为的"类”。
编码角度:肯定是先有类了,不然new 什么呢?

b.类与类

类是抽象的。是抽象的"类别"。类与类行为不同。

什么是行为?我的理解其实就是代码,如果一个类的代码改变了,那么它的逻辑也改变,逻辑改变体现在对象的实际执行上,也就是对象的行为也会因为逻辑的改变而改变。

那么不同类的代码肯定是不一样的,所以行为也不一样。

c.对象与对象

对象和对象数据不同。
不同类的对象就不说了。同类型的对象呢?

d.区分是对象不同还是类不同

比如人这个类。

游戏的捏脸功能就是如此,修改一些不同的属性数据即可。比如有的人腿长,有的人腿短,只是数据不同。

但是如果需求要人能飞,能射出蛛丝,“一般"来说我们就得新建一个类了。给它加上 飞行/喷射蛛丝 的行为。

所以,对象和对象的不同,只是数据上,比如腿的长短。而类就得是 行为/功能 上的不同。

  • 类与类行为不同;
  • 对象与对象数据不同。

2.主要思想

  1. 分而治之
    将一个大的需求分解为许多类,每个类处理一个独立的模块。
    拆分好处:独立模块便于分工,每个模块便于复用,可扩展性强。

  2. 封装变化
    变化的地方独立封装,避免影响其他模块。

  3. 高内聚
    类中各个方法都在完成一项任务(单一职责的类)。
    复杂的实现封装在内部,对外提供简单的调用。

  4. 低耦合
    类与类的关联性依赖度要低(每个类独立)。
    让一个模块的改变,尽少影响其他模块。

1).分而治之和封装变化

分而治之其实就是模块化。

封装变化:
比如我们可以攻击,但是攻击可以分为用手,用武器,等。那么我们就应该用一个专门的武器类来封装这些。

难点在于,什么时候该用类来封装。老师的说法是,变化。
就比如攻击,当攻击方式多样化的时候,就应该封装了。

2).高内聚和低耦合

也很简单。老师也列了一个例子。

[例如:硬件高度集成化,又要可插拔]
最高的内聚莫过于类中仅包含1个方法,将会导致高内聚高耦合。
最低的耦合莫过于类中包含所有方法,将会导致低耦合低内聚。

  1. 高内聚高耦合
    说的是,如果类都是最简单的一个方法那种,是高内聚了,但是实现一个功能,往往不可能这么简单,那么就得调用多个类,导致高耦合。

  2. 低耦合低内聚
    极端的一个类囊括所有功能。低耦合的确,但是各种功能都在一个类,导致了低内聚。

2.继承

复习一些基础知识

1).栈和堆

深入理解堆栈、堆在内存中的实现

什么是堆?什么是栈?他们之间有什么区别和联系?

推荐看这篇知乎回答。

在这里插入图片描述

  1. 管理方式:栈由编译器自动管理,无需人为控制。而堆释放工作由程序员控制,容易产生内存泄漏(memory leak)。

  2. 空间大小:在32位系统下,堆内存可以达到4G的空间(虚拟内存的大小,有面试官问过),从这个角度来看堆内存大小可以很大。但对于栈来说,一般都是有一定的空间大小的。

  3. 碎片问题:堆频繁new/delete会造成内存空间的不连续,造成大量的碎片,使程序效率降低(如何解决?如内存池、伙伴系统等)。对栈来说不会存在这个问题,因为栈是先进后出,不可能有一个内存块从栈中间弹出。在该块弹出之前,在它上面的(后进的栈内容)已经被弹出。

  4. 生长方向:堆生长(扩展)方向是向上的,也就是向着内存地址增加的方向;栈生长(扩展)方向是向下的,是向着内存地址减小的方向增长, 可以看第一张图。

  5. 分配方式:堆都是动态分配的,没有静态分配的堆。而栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,如局部变量分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手工实现。

  6. 效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持(有专门的寄存器存放栈的地址,压栈出栈都有专门的机器指令执行),这就决定了栈的效率比较高。堆则是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. 虚方法: 父类型的引用 指向 子类的对象,调用虚方法,执行子类中的重写方法。
  2. 抽象方法:抽象类的引用 指向 实现类的对象,调用抽象方法,执行实现类中重写方法。
  3. 接口:接口的引用 指向 实现类的对象,调用接口方法,执行实现类中实现方法。

1).方法重写

语法:在子类中使用override关键字修饰的方法。
作用:父类的方法在子类中不适用(虚方法),或父类没有实现(抽象方法)。子类重写可以满足对该方法的不同需求。方法重写时必须在方法前加override关键字。

a.三种方法可以重写:
  • abstract 方法在子类必须重写,除非子类也是抽象类。
  • virtual 方法在子类可以重写,父类方法的做法与子类不同。
  • override 方法,已经重写过的方法,在子类还可以继续重写,除非被标识为sealed。(多重继承)
b.重写原理
  1. 子类在方法表中修改对应的地址;
  2. 修改父级方法表地址。

不管通过父类还是子类型的引用,调用方法时,都执行对象真实类型中定义的方法。

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.作用:
  1. 解决多接口实现时的二义性
  2. 解决接口中的成员对实现类不适用的问题

比如两个接口,都有同一个名称的方法。这时候如何实现,如何调用呢?

b.做法:

在这里插入图片描述

  • 在实现的成员前加接口名,并且不能加任何访问修饰符,默认为private
  • 显式实现成员只能通过接口类型的引用调用。
    Void InterFace1.Fun()
    { }
c.使用场景
  1. 两个接口同个方法名,这时候用,这两个方法都会变成私有。只有父类引用才能调用的到(父类肯定不是私有啊);
  2. 这个方法,对于我这个类,并不需要实现且暴露给外部。也是变成了私有,外部调不到。

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 的对象:

  1. null 或者 数字 —> 等待一个渲染帧
  2. new WaitForSeconds(1) --> 等待指定时间
  3. new WaitForSecondsRealtime(1) --> 等待指定时间(不受时间缩放影响)
  4. new WaitForFixedUpdate() --> 等待一个物理帧
  5. new WaitForEndOfFrame() --> 等待一帧结束
  6. new WaitWhile( 委托 ) —> (下回分解……)
  7. Coroutine --> 在另一个协程函数执行完毕后再执行。
  8. 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.流程
  1. 得到数据类型
  2. 动态创建对象
  3. 查看类型信息(了解本身信息,成员信息)

具体实现流程,请看 4.

2).常用类

a.取得数据类型Type
方式:
  1. Type.GetType(“类型全名”):适合于类型的名称已知。
  2. obj.GetType():适合于类型名未知, 类型未知,存在已有对象。
  3. typeof(类型):适合于已知类型。
  4. Assembly.Load(“XXX”).GetType(“名字”):适合于类型在另一个程序集中。 Untiy中目前基本不使用。
b.Type类常用Get系列方法 Is系列属性。
  1. MethodInfo(方法)
    重要方法: Invoke
  2. PropertyInfo(属性)
    重要方法:SetValue GetValue
  3. FieldInfo(字段)
    重要方法:SetValue GetValue
  4. 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. 首先分析需求,老师已经帮我们分析好了。如图。
  2. 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的这种写法,等以后实际使用时再慢慢消化吧。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Unity中,要使用android.permission.READ_LOGS权限,需要根据不同的Unity版本和Bugly Unity Plugin版本进行相应的配置。根据引用[1]中的Android SDK使用指南,可以修改导出的Android工程的AndroidManifest.xml文件,将android.permission.READ_LOGS权限添加到权限声明中。具体步骤如下: 1. 打开Unity项目工程。 2. 导入最新版本的Bugly Unity Plugin。根据引用中的通用部分集成步骤,下载并导入Bugly Unity Plugin的相关文件到您的Unity工程中。 3. 打开导出的Android工程的AndroidManifest.xml文件。这个文件位于Unity项目工程的Assets/Plugins/Android目录下。 4. 在AndroidManifest.xml文件中的权限声明部分,添加如下权限: <uses-permission android:name="android.permission.READ_LOGS" /> 通过以上步骤,您就可以将android.permission.READ_LOGS权限添加到Unity项目的AndroidManifest.xml文件中,以实现读取logcat日志的功能。请注意,根据具体的Bugly Unity Plugin版本和Unity版本,可能还需要执行其他配置和集成步骤,具体可以参考Bugly Unity Plugin的官方文档或相关资源。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Bugly Unity Plugin](https://blog.csdn.net/qq_39816832/article/details/80238872)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [unity--关于自动添加READ_PHONE_STATE权限](https://blog.csdn.net/lalate/article/details/84340644)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值