知识点记录(Unity/C#)

  1. 1. Shader
  2. 2. 算法
  3. 3. oop的四个特征
  4. 4. 迭代器和数组的区别
  5. 5. 泛型的作用。
  6. 6. 性能优化
  7. 7. C#语法
    1. 7.1. C# 特性(Attribute)
    2. 7.2. 特殊类
    3. 7.3. 深拷贝
  8. 8. 反射的作用
  9. 9. Prefab
  10. 10. 委托和事件的区别
  11. 11. 万向锁
  12. 12. Unity生命周期函数
  13. 13. AssetBundle
  14. 14. protobuf
  15. 15. 重写和重载的区别
  16. 16. 堆(Heap)和栈(Stack)
  17. 17. GC
  18. 18. Lua
  19. 19. 网络同步

Shader
  1. 从Render获取材质,获得材质的Shader,获取Shader的Uniform Constant值,每帧修改constant值来实现装备的流光效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Material material = Render.materials;
    material.shader = targetShader;
    //修改 Shader 中参数的值:
    material.SetFloat(“参数名”,值);
    material.SetColor(“参数名”,颜色值);
    material.SetTexture(“参数名”,贴图);

    //获取 Shader 中参数的值:
    material.GetFloat(“参数名”);
    material.GetColor(“参数名”);
    material.GetTexture(“参数名”);
算法
  1. 给定一个数组,有n(n >= 0)个元素,找到数组中第二大的元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    class Program
    {
    static void Main(string[] args)
    {
    }
    //1 5 9 2 1 6 4 2 3 9
    int GetScendNum(int[] number, int n)
    {
    if(n < 2)
    Console.WriteLine("不存在")
    else
    {
    int first=number[0],second=number[0];
    for(int i=0;i<n;++i){
    if(number[i]>first){
    second=first;
    first=number[i];
    }
    else{
    if(number[i]>second)
    second=number[i];
    else{
    // 若第二和第一相等,则将当前值赋予第二
    second = first==second ? number[i] : second;
    }
    }
    }
    }
    }
    }
  2. 给定一个可包含重复数字的序列 nums ,按序列内字典升序返回所有不重复的全排列。

    其中序列内字典升序指的是, 序列内从左到右的非降序排列,例如 nums=[1,2,3], 则因为[1,2,3] < [1,3,2], [3,1,2] < [3,2,1], [1,2,3]要先于[1,3,2]输出,[3,1,2]要先于[3,2,1]输出

    1. 使用STL库用来计算排列组合关系的算法:next_permutation和prev_permutation。

      即按字典序(lexicographical)来找到下一个或前一个的排列组合。例:{2,1,3}的下一个是{2,3,1},前一个是{1,3,2}。字典序即两个组合从左往右依次比较,若某一位A比B小,则A的字典序在B之前。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      vector<vector<int> > UniquePerm(vector<int>& nums) {
      sort(nums.begin(), nums.end());
      vector<vector<int> > res;
      do
      {
      res.push_back(nums);
      //测试
      for (auto num : nums)
      {
      cout << num << " ";
      }
      cout << endl;
      //
      } while (next_permutation(nums.begin(), nums.end()));
      res.shrink_to_fit();
      return res;
      }
oop的四个特征

抽象、封装、继承、多态

  • 抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节。比如,我们要设计一个学生成绩管理系统,考察学生这个对象时,我们只关心他的班级、学号、成绩等,而不用去关心他的身高、体重这些信息。抽象包括两个方面,一是过程抽象,二是数据抽象。过程抽象是指任何一个明确定义功能的操作都可被使用者看作单个的实体看待,尽管这个操作实际上可能由一系列更低级的操作来完成。数据抽象定义了数据类型和施加于该类型对象上的操作,并限定了对象的值只能通过使用这些操作修改和观察。
  • 继承是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。这也体现了大自然中一般与特殊的关系。继承性很好的解决了软件的可重用性问题。比如说,所有的Windows应用程序都有一个窗口,它们可以看作都是从一个窗口类派生出来的。但是有的应用程序用于文字处理,有的应用程序用于绘图,这是由于派生出了不同的子类,各个子类添加了不同的特性。
  • 封装是面向对象的特征之一,是对象和类概念的主要特性。封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。面向对象计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。一旦定义了一个对象的特性,则有必要决定这些特性的可见性,即哪些特性对外部世界是可见的,哪些特性用于表示内部状态。在这个阶段定义对象的接口。通常,应禁止直接访问一个对象的实际表示,而应通过操作接口访问对象,这称为信息隐藏。事实上,信息隐藏是用户对封装性的认识,封装则为信息隐藏提供支持。封装保证了模块具有较好的独立性,使得程序维护修改较为容易。对应用程序的修改仅限于类的内部,因而可以将应用程序修改带来的影响减少到最低限度。
  • 多态性是指的是同一接口的不同实现方式,多态允许基类的指针指向子类方法。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序函数同名问题。

    • 多态的作用:

      • 不必编写每一子类的功能调用,可以直接把不同子类当父类看,屏蔽子类间的差异,提高代码的通用率/复用率
  • 父类引用可以调用不同子类的功能,提高了代码的扩充性和可维护性
迭代器和数组的区别

迭代器:迭代器(iterator)有时又称光标(cursor)是程序设计的软件设计模式,可在容器对象(container,例如链表数组)上遍访的接口,设计人员无需关心容器对象的内存分配的实现细节。

  • C#

    • 迭代器模式 指按照一定顺序来访问一个集合对象中的每个元素, 但是同时不会暴露集合对象的内部结构. C#中内置的迭代器模式就是Foreach语句, 它可以顺序遍历容器中的每个元素. 而迭代器的具体实现主要是靠IEnumerable 和IEnumerator.

    • IEnumerator

      IEnumerator接口其实就是foreach的具体实现, 它只定义了三个函数, 如下

      1
      2
      3
      4
      5
      6
      7
      8
      public interface IEnumerator
      {
      bool MoveNext();

      object Current { get; }

      void Reset();
      }

      也就是说我们实现一个最简单的IEnumerator接口只需要实现这三个函数即可. MoveNext()表示向集合中的下一个元素移动, 如果有下一个元素返回true, 没有就返回false. Current是一个只读属性, 返回当前迭代器所指元素. Reset()表示重置迭代器到第一个元素.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      class MyIEnumerator: IEnumerator
      {
      private int count = 10;
      public bool MoveNext()
      {
      --count;
      return count >= 0;
      }

      public object Current
      {
      get { return count; }
      }

      public void Reset()
      {
      count = 10;
      }
      }

      这就是一个简单的IEnumerator, 如果用Foreach输出, 可以得到从9到0的十个计数. 而具体的实现过程还需要IEnumerable.

    • IEnumerable

      IEnumerable可以粗略的理解为可迭代(遍历)的, 如果接口继承了IEnumerable, 那么就可以使用Foreach语句进行迭代操作. 这个接口只定义了一个函数, 如下:

      1
      2
      3
      4
      public interface IEnumerable
      {
      IEnumerator GetEnumerator();
      }

      这个函数需要返回一个我们刚才定义的IEnumerator, 即告知上层调用方可以枚举.

      1
      2
      3
      4
      5
      6
      7
      class MyIEnumerable : IEnumerable
      {
      public IEnumerator GetEnumerator()
      {
      return new MyIEnumerator();
      }
      }

       这个时候我们的迭代器已经实现, 可以使用Foreach语句进行迭代操作.

      1
      2
      3
      4
      5
      6
      7
      void Start()
      {
      foreach (var VARIABLE in new MyIEnumerable())
      {
      Debug.Log(VARIABLE);
      }
      }

      我们现在有一个完整的迭代器了, 但是有一个问题, 很多时候我们需要的迭代器并不复杂, 如上面的计数装置, 如果每次都需要实现IEnumerator和IEnumerable十分不方便, 显得头重脚轻, 这时我们可以使用yield.

    • yield

      yield是为了方便使用迭代器而产生的语法糖, 他可以直接使用在返回类型为IEnumerable或IEnumerator的函数中直接实现迭代器操作. 它有两种用法yield break 和yield return (something) 效果和break与return一样. 当使用yield return在foreach语句中进行迭代器操作时, 每一次执行到yield return时都会返回后面定义的something并且记录函数内的信息, 下一次运行时继续.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      void Start()
      {
      foreach (var VARIABLE in myIEnumerable())
      {
      Debug.Log(VARIABLE);
      }
      }

      private IEnumerable myIEnumerable()
      {
      for (int i = 9; i >= 0; --i)
      {
      if (i == 5) yield break;
      yield return i;
      }
      }

      这里我们在i = 5的时候使用了yield break终止了迭代器操作, 如果去掉它, 这个函数就和我们刚才定义的迭代器作用一样, 产生了9到0, 共10个数的倒数.

    • 协程(coroutine)

      除了foreach语句, Unity中另一种迭代器模式就是协程, 它是根据每一次IEnumerator的MoveNext()方法调用进行迭代的, 有些类似于Unity的Update方法. 同时它可以结合Unity的yield return new WaitForSeconds(time)等等接口实行迭代器+定时器模式.
      使用和停止协程比较简单, 都只要传入IEnumerator就可以.

      1
      2
      public Coroutine StartCoroutine(IEnumerator routine)
      public void StopCoroutine(IEnumerator routine)

      使用协程的方法实现我们的从9数到0.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      void Start()
      {
      StartCoroutine(myCoroutine());
      }

      private IEnumerator myCoroutine()
      {
      for (int i = 9; i >= 0; i--)
      {
      Debug.Log(i);
      yield return new WaitForSeconds(1f);
      }

      yield return StartCoroutine(myCoroutine());
      }

        值得注意的是unity里有很多可以和yield结合使用的语句块, 如上的代码不仅实现了从9数到0, 还实现了每隔1秒数一次, 数到0后继续从头开始数, 当然还有很多其他有趣的功能可以通过coroutine和yield实现.

泛型的作用。

泛型和传统类型Boxing/UnBoxing的区别

  1. 泛型:即通过参数化类型来实现在同一份代码上操作多种数据类型。泛型编程是一种编程范式,它利用“参数化类型”将类型抽象化,从而实现更为灵活的复用。

  2. 作用:减少拆装箱,确保类型安全

  3. 区别:

    • 装箱:值类型转引用类型。开辟一块内存空间进行存放数据。
    • 拆箱:引用类型转值类型。
  4. 写一个泛型类 C 使其继承自 CBase

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class CBase
    {

    }

    public class C<T> : CBase
    {
    public T t;
    }

    public class C1<T> where T : CBase
    {
    public T t;
    }
性能优化
  1. 当你接手一个新项目,当运行一段时间后,发现内存占用高且帧率低,如何解决

    性能优化:

C#语法
C# 特性(Attribute)

特性(Attribute)是用于在运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)的行为信息的声明性标签。可以通过使用特性向程序添加声明性信息。一个声明性标签是通过放置在它所应用的元素前面的方括号([ ])来描述的。

特性(Attribute)用于添加元数据,如编译器指令和注释、描述、方法、类等其他信息。.Net 框架提供了两种类型的特性:预定义特性和自定义特性。

规定特性(Attribute)的语法如下:

1
2
3
4
5
[attribute(positional_parameters, name_parameter = value, ...)]
element

//特性(Attribute)的名称和值是在方括号内规定的,放置在它所应用的元素之前。
//positional_parameters 规定必需的信息,name_parameter 规定可选的信息。

预定义特性(Attribute)

.Net 框架提供了三种预定义特性:

  • AttributeUsage:描述了如何使用一个自定义特性类。它规定了特性可应用到的项目的类型。

    规定该特性的语法如下:
    [AttributeUsage( validon, AllowMultiple=allowmultiple,Inherited=inherited)]

    1. 参数 validon规定特性可被放置的语言元素。它是枚举器AttributeTargets的值的组合。默认值是AttributeTargets.All。
    2. 参数allowmultiple(可选的)为该特性的AllowMultiple属性(property)提供一个布尔值。如果为true,则该特性是多用的。默认值是false(单用的)
    3. 参数inherited(可选的)为该特性的Inherited 属性(property)提供一个布尔值。如果为true,则该特性可被派生类继承。默认值是false(不被继承)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /*
    * AttributeUsage(AttributeTargets.Class)//只能使用在类上
    * AttributeUsage(AttributeTargets.Method)//只能使用在方法上
    */
    //AttributeTargets.Method|AttributeTargets.Class表示可以在类上用也可以在方法上用
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]//AllowMultiple =true可以在方法上写多个[C1]
    class C1 : Attribute
    {
    }

    class C2
    {
    //使用
    [C1]
    public void getStr()
    {

    }
    }
  • Conditional:我们可以将一些函数隔离出来,使得它们只有在定义了某些环节变量或者设置了某个值之后才能发挥作用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    //若把 #define DEBUG 注释,则Message方法不会执行。即Conditional特性只会只会在其包含的”DEBUG“定义后才能执行
    #define DEBUG
    using System;
    using System.Diagnostics;
    public class Myclass
    {
    [Conditional("DEBUG")]
    public static void Message(string msg)
    {
    Console.WriteLine(msg);
    }
    }
    class Test
    {
    static void function1()
    {
    Myclass.Message("In Function 1.");
    function2();
    }
    static void function2()
    {
    Myclass.Message("In Function 2.");
    }
    public static void Main()
    {
    Myclass.Message("In Main function.");
    function1();
    Console.ReadKey();
    }
    }
  • Obsolete:它标记了不应被使用的程序实体。当一个新方法被用在一个类中,但若仍然想要保持类中的旧方法,可以通过显示一个应该使用新方法,而不是旧方法的消息,来把它标记为 obsolete(过时的)。

    1
    2
    3
    4
    5
    6
    7
    [Obsolete(
    message
    )]
    [Obsolete(
    message,
    iserror
    )]
    • 参数 message,是一个字符串,描述项目为什么过时以及该替代使用什么。
    • 参数 iserror,是一个布尔值。如果该值为 true,编译器应把该项目的使用当作一个错误。默认值是 false(编译器生成一个警告)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    using System;
    public class MyClass
    {
    [Obsolete("Don't use OldMethod, use NewMethod instead", true)]
    static void OldMethod()
    {
    Console.WriteLine("It is the old method");
    }
    static void NewMethod()
    {
    Console.WriteLine("It is the new method");
    }
    public static void Main()
    {
    OldMethod();
    }
    }
    //当使用OldMethod方法时,编译器会报错,错误信息为Obsolete的内容
特殊类
  1. 密封类:不能被继承

    • 关键字:sealed
  2. 接口类:接口定义了所有类继承]接口时应遵循的语法合同

    • Interface
  3. 抽象类:不能被实例化,可以包含非抽象成员

    • abstract

      1
      2
      3
      4
      5
      // 抽象方法只需要声明
      // 抽象方法
      public abstract double GetName();
      // 抽象属性
      public abstract string Name { get; }
深拷贝
  • 利用反射实现:此方式比较耗费性能,而且遇到对象中有值为null就会报错

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public static T DeepCopy<T>(T obj)
    {
    //如果是字符串或值类型则直接返回
    if (obj is string || obj.GetType().IsValueType) return obj;

    object retval = Activator.CreateInstance(obj.GetType());
    FieldInfo[] fields = obj.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
    foreach (FieldInfo field in fields)
    {
    try { field.SetValue(retval, DeepCopy(field.GetValue(obj))); }
    catch { }
    }
    return (T)retval;
    }
  • 二进制序列化实现:性能相对较高,遇到对象为null不会报错,需要在拷贝对象上标记[serializable]特性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public static T DeepCopyByBinary<T>(T obj)
    {
    object retval;
    using (MemoryStream ms = new MemoryStream())
    {
    BinaryFormatter bf = new BinaryFormatter();
    bf.Serialize(ms, obj);
    ms.Seek(0, SeekOrigin.Begin);
    retval = bf.Deserialize(ms);
    ms.Close();
    }
    return (T)retval;
    }
  • xml序列化实现:遇到对象为null不会报错

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public T DeepCopy<T>(T obj)
    {
    object retval;
    using (MemoryStream ms = new MemoryStream())
    {
    XmlSerializer xml = new XmlSerializer(typeof(T));
    xml.Serialize(ms, obj);
    ms.Seek(0, SeekOrigin.Begin);
    retval = xml.Deserialize(ms);
    ms.Close();
    }
    return (T)retval;
    }
  • 利用silverlight DataContractSerilalizer实现

  • 利用protobuf序列化实现

反射的作用

动态创建一个数据集,并获得其类型T,调用T的Create函数。

  • .NET的一个强大功能是它可以通过一种称为反射(reflection)的过程访问应用程序的元数据。简单地说,反射就是运行时查询类型信息的能力。可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型。然后,可以调用类型的方法或访问其字段和属性。

  • 反射(Reflection)有下列用途:

    • 它允许在运行时查看特性(attribute)信息。
    • 它允许审查集合中的各种类型,以及实例化这些类型。
    • 它允许延迟绑定的方法和属性(property)。
    • 它允许在运行时创建新类型,然后使用这些类型执行一些任务。
  • 优点:

    • 1、反射提高了程序的灵活性和扩展性。
    • 2、降低耦合性,提高自适应能力。
    • 3、它允许程序创建和控制任何类的对象,无需提前硬编码目标类。

    缺点:

    • 1、性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此反射机制主要应用在对灵活性和拓展性要求很高的系统框架上,普通程序不建议使用。
    • 2、使用反射会模糊程序内部逻辑;程序员希望在源代码中看到程序的逻辑,反射却绕过了源代码的技术,因而会带来维护的问题,反射代码比相应的直接代码更复杂。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class Program
    {
    public static void Main()
    {
    Rectangle r = new Rectangle(4.5, 7.5);
    r.Display();
    Type type = typeof(Rectangle);

    // 遍历 Rectangle 类的特性
    foreach (DeBugInfo attributes in type.GetCustomAttributes(false))
    {
    if (null != attributes)
    {
    Console.WriteLine("Bug no: {0}", attributes.BugNo);
    Console.WriteLine("Developer: {0}", attributes.Developer);
    Console.WriteLine("Last Reviewed: {0}",
    attributes.LastReview);
    Console.WriteLine("Remarks: {0}", attributes.Message);
    }
    }
    Console.ReadKey();
    }
    }
Prefab

如何获得Prefab的GameObject类型,PrefabInstance和ModelPrefabInstance的区别

委托和事件的区别
  1. 委托类似于 C 或 C++ 中函数的指针。委托(Delegate) 是存有对某个方法的引用的一种引用类型变量。引用可在运行时被改变。

    委托(Delegate)特别用于实现事件和回调方法。所有的委托(Delegate)都派生自 System.Delegate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    //声明语法
    delegate <return type> <delegate-name> <parameter list>

    //使用
    class TestDelegate
    {
    static int num = 10;
    public static int AddNum(int p)
    {
    num += p;
    return num;
    }

    public static int MultNum(int q)
    {
    num *= q;
    return num;
    }
    public static int getNum()
    {
    return num;
    }

    static void Main(string[] args)
    {
    // 创建委托实例 : 新建委托对象,传入函数签名和委托类型一致的函数
    NumberChanger nc1 = new NumberChanger(AddNum);
    NumberChanger nc2 = new NumberChanger(MultNum);
    // 使用委托对象调用方法,调用委托,传入参数
    nc1(25);
    Console.WriteLine("Value of Num: {0}", getNum());
    nc2(5);
    Console.WriteLine("Value of Num: {0}", getNum());
    Console.ReadKey();
    }
    }
  2. 事件(Event) 基本上说是一个用户操作,如按键、点击、鼠标移动等等,或者是一些提示信息,如系统生成的通知。应用程序需要在事件发生时响应事件。例如,中断。

    C# 中使用事件机制实现线程间的通信。

    通过事件使用委托

    • 事件在类中声明且生成,且通过使用同一个类或其他类中的委托与事件处理程序关联。包含事件的类用于发布事件。这被称为 发布器(publisher) 类。其他接受该事件的类被称为 订阅器(subscriber) 类。事件使用 发布-订阅(publisher-subscriber) 模型。

    • 发布器(publisher) 是一个包含事件和委托定义的对象。事件和委托之间的联系也定义在这个对象中。发布器(publisher)类的对象调用这个事件,并通知其他的对象。

    • 订阅器(subscriber) 是一个接受事件并提供事件处理程序的对象。在发布器(publisher)类中的委托调用订阅器(subscriber)类中的方法(事件处理程序)

    1
    2
    3
    4
    5
    6
    //声明委托
    public delegate void BoilerLogHandler(string status);

    //基于上面的委托定义事件, 事件会在生成时调用委托
    public event BoilerLogHandler BoilerEventLog;

  3. 区别

    1. 委托

      • Delegate 是一个类,在任何可以声明类的地方都可以声明委托。
      • 可以将多个方法赋给同一个委托,或者叫将多个方法绑定到同一个委托,当调用这个委托的时候,将依次调用其所绑定的方法。
    2. 事件

      • 在类的内部,它总是private 的。

      • 在类的外部,注册“+=”和注销“-=”的访问限定符与你在声明事件时使用的访问符相同。

      • 使用事件不仅能获得比委托更好的封装性以外,还能限制含有事件的类型的能力。

      • 事件应该由事件发布者触发,而不应该由事件的客户端(客户程序)来触发。

万向锁

万向锁(Gimbal Lock):一旦选择±90°作为pitch角,就会导致第一次旋转和第三次旋转等价,整个旋转表示系统被限制在只能绕竖直轴旋转,丢失了一个表示维度。

以unity为例:unity中欧拉角的旋转顺序为 y-x-z。即旋转y轴x轴和z轴都改变,旋转x轴z轴改变,旋转z轴其他轴不变。

当模型的x轴的旋转为±90度时,旋转y轴和z轴的效果是一样的,这就是万向锁。此时y轴(惯性坐标系)旋转面和z轴(模型坐标系)旋转面共面

因为计算机每次执行旋转都是从[0, 0, 0]开始进行,且y轴旋转的优先级最高,此时y轴的模型坐标系和惯性坐标系的重合,因此y轴的旋转从结果上看就是惯性坐标系的旋转。

Unity生命周期函数

Awake -> OnEnable -> Start -> FixedUpdate -> OnTrigger/OnCollision -> 输入事件 -> Update -> LateUpdate -> 渲染(Scene -> Gizmo -> GUI) -> OnDisable -> OnDestroy

AssetBundle

AssetBundle是Unity中的一种资源包,这种资源包可以是游戏内要用到的几乎所有资源,并且可以在运行时动态加载。

  1. 卸载Load的asset资源,通过Resources.UnloadAsset(asset)来进行卸载;
  2. 压缩格式:LZMA, LZ4, 以及不压缩。

    1. LZMA是一种默认的压缩形式,这种标准压缩格式是一个单一LZMA流序列化数据文件,并且在使用前需要解压缩整个包体。能使压缩后文件达到最小,但是解压相对缓慢。
    2. LZ4能使得压缩量更大,而且在使用资源包前不需要解压整个包体。
    3. 不压缩的方式打包后包体会很大,导致很占用空间
  3. AssetBundle的卸载和加载

    • 加载

      1. AssetBundle.LoadFromFile:从本地加载

        1
        2
        3
        4
        5
        6
        private void Start()
        {
        AssetBundle ab = AssetBundle.LoadFromFile("ab包名字/所在目录/资源名称.后缀");
        GameObject gameObj= ab.LoadAsset<GameObject>("资源名称");
        Instantiate(gameObj); //实例化
        }
      2. AssetBundle.LoadFromMemory:从内存加载

        1
        2
        3
        4
        5
        6
        private void Start()
        {
        AssetBundle ab = AssetBundle.LoadFromMemory(File.ReadAllBytes("ab包名字/所在目录/资源名称.后缀"));
        GameObject gameObj= ab.LoadAsset<GameObject>("资源名称");
        Instantiate(gameObj); //实例化
        }
      3. AssetBundle.LoadFromMemoryAsync:从内存异步加载

        1
        2
        3
        4
        5
        6
        7
        8
        IEnumerator Start()
        {
        AssetBundleCreateRequest request = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes("ab包名字/所在目录/资源名称.后缀"));
        yield return request;
        AssetBundle ab = request.assetBundle;
        GameObject obj = ab.LoadAsset<GameObject>("资源名称");
        Instantiate(obj);
        }
      4. 从AB中加载资源

        AssetBundle.LoadAsset(assetName) :加载AB包中的指定对象,不包含依赖的包 AssetBundle.LoadAllAssets() :加载AB包中所有的对象,不包含依赖的包 AssetBundle.LoadAssetAsync() :异步加载,加载较大资源的时候 AssetBundle.LoadAllAssetsAsync() :异步加载全部资源 AssetBundle.LoadAssetWithSubAssets() :加载资源及其子资源

    • 卸载

      • AssetBundle.Unload(true):卸载所有资源,包含其中正被使用的资源
      • AssetBundle.Unload(false):卸载所有没被使用的资源
      • Resources.UnloadUnusedAssets():卸载未使用的资源
protobuf
重写和重载的区别

​ 1.定义不同—-重载是定义相同的方法名,参数不同;重写是子类重写父类的方法。

​ 2.范围不同—-重载是在一个类中,重写是子类与父类之间的。

​ 3.多态不同—-重载是编译时的多态性,重写是运行时的多态性。

​ 4.返回不同—-重载对返回类型没有要求,而重写要求返回类型,有兼容的返回类型。

​ 5.参数不同—-重载的参数个数、参数类型、参数顺序可以不同,而重写父子方法参数必须相同。

​ 6.修饰不同—-重载对访问修饰没有特殊要求,重写访问修饰符的限制一定要大于被重写方法的访问修.

堆(Heap)和栈(Stack)
  1. 程序内存

    • 栈:由操作系统自动分配释放,用于存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈。:堆由开发人员分配和释放, 若开发人员不释放,程序结束时由 OS 回收,分配方式类似于链表。

    • 区别:

      • 管理方式不同。栈自动释放;堆手动释放,容易产生内存泄漏
      • 空间大小不同。栈 << 堆。
      • 生长方向不同。堆的内存地址由低到高;栈的内存地址由高到低。
      • 分配方式不同。堆都是动态分配的;栈有静态分配和动态分配。
  2. 数据结构

    • 栈:线性表。先进后出(First In Last Out).
    • 堆:树形结构,是一种特殊的完全二叉树。满足所有节点的值总是不大于或不小于父节点的值的完全二叉树。根节点最大称为大顶堆,根节点最小称为小顶堆。
  3. 值类型存储在栈中;引用类型存储在堆上。

GC

​ 什么是GC:即Garbage Collection,垃圾回收。当使用可用内存不能满足内存请求时,GC会自动进行。

  1. C#中的GC

    1. GC的四个步骤。

      • 垃圾回收器搜索内存中的托管对象;

      • 从托管代码中搜索被引用的对象并标记为有效;

      • 释放没有被标记为有效的对象并收回内存;

      • 整理内存将有效对象挪动到一起。

    2. GC的作用:

      • 提高软件系统的内聚。

      • 降低编程复杂度,使程序员不必分散精力去处理析构。

      • 不妨碍设计师进行系统抽象。

      • 减少由于内存运用不当产生的Bug。

      • 成功的将内存管理工作从程序的编写时,脱离至运行时,使不可预估的管理漏洞变为可预估的。

    3. 什么是垃圾

      只要判定一个引用类型对象或者其包含的子对象没有任何引用是有效的,那么系统就认为它是垃圾。

    4. 对象代龄

      CLR初始化后的第一批被创建的对象被列为0代对象。

    5. 回收方法

      • Finalizer(析构函数):一般不用,因为不确定GC调用时间,影响效率。

      • Dispose:继承IDisposable接口,实现Dispose方法;调用Dispose方法,销毁对象,需要显示调用或者通过using语句,在显示调用或者离开using程序块时被调用。’

      • Mark-Compact 标记压缩算法

      • Generational 分代算法

  2. Unity中的CG

    1. Boehm GC(非分代非压缩)
      1. Non-generational(非分代式),即全都堆在一起,因为这样会很快。分代的话就是例如大内存,小内存,超小内存分在不同的内存区域来进行管理(SGen GC的设计思想)。
      2. Non-Compacting(非压缩式),即当有内存被释放的时候,这块区域就空着。而压缩式的会重新排布,填充空白区域,使内存紧密排布。
    2. 降低GC的方法:
      1. 使用对象池
      2. 减少string,使用StringBuilder
      3. 减少拆装箱
      4. struct中不要有引用类型变量
      5. 主动调用GC
  3. 三色标记清除法

    1. 三色:黑色、灰色、白色
      • 黑色: 表示对象以及被垃圾收集器访问过,且这个对象的引用都已经扫描过。黑色的对象代表以及扫描过,他是安全存活的,如果有其他对象引用指向了黑色对象,无需重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
      • 灰色: 表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描到
      • 白色: 表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色,如在分析结束的阶段,仍然是白色的对象,即代表不可达。
    2. 步骤:
      1. 刚开始,所有对象都在白色集合
      2. 将GC Roots直接引用的对象挪到灰色集合
      3. 灰色集合中获取对象:
        • 将本对象的引用到的对象放入灰色集合
        • 将本对象放入黑色集合
      4. 重复步骤3,直到灰色集合为空结束
      5. 结束后,仍在白色集合的对象即为GC Roots不可达,可以进行回收。
  4. 三代标记清除法

Lua
  1. pairs和ipairs

    • 同:都能遍历集合,均按优先顺序输出没有key的值

    • 异:对于又key的值

      ipairs从第一个数字key开始,依次输出所有的key+1的键值,遇到字母下标不会结束遍历,但不输出,如果遇到nil则退出;
      pairs无序输出字母类型key或者数字类型key的键值,遇到nil不输出,也不会停止遍历。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    table = { [1] = "test3", [2] = "val1" , [5] = "val2", [4] = "val4" }
    print("-----------ipairs----------------")
    for k,v in ipairs(table) do
    print(k,v)
    end
    print("-----------pairs----------------")
    for k,v in pairs(table) do
    print(k,v)
    end

    --输出
    -----------ipairs----------------
    1 test3
    2 val1
    -----------pairs----------------
    4 val4
    1 test3
    2 val1
    5 val2
    --结论
    1ipairs会按照key的顺序输出数据,遇到不连续的数据停止输出;
    2pairs会无序输出所有数据;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    table = { [3] = "test3", ["test"] = "val1", "val3" , [4] = "val2", "val4" }
    print("-----------ipairs----------------")
    for k,v in ipairs(table) do
    print(k,v)
    end
    print("-----------pairs----------------")
    for k,v in pairs(table) do
    print(k,v)
    end

    --输出
    -----------ipairs----------------
    1 val3
    2 val4
    3 test3
    4 val2
    -----------pairs----------------
    1 val3
    2 val4
    4 val2
    test val1
    3 test3

    --结论
    1pairsipairs均优先输出没有key的value;
    2pairs会输出所有的数据,不带key的值按顺序输出,带key的值无序输出;
    3ipairs会跳过字符串的key,按顺序输出数字型key的值;
  2. __index和__newindex

    • __index:(get)当在表中找不到元素时,lua会从 __index指向的表查找元素或获得方法的返回值。

    • __newindex:(set)对表中不存在的值进行赋值时调用。

      当__newindex指向一个函数时,会执行该函数,且对本表的创建不成功;

      当__newindex指向一个表时,会对指向的表进行操作,且对本表创建不成功。

  3. __rawset和 __rawget

    可以通过rawset和rawget操作绕过原表这一过程(__index和__newindex),直接把这个表相应的结论输出。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    local table1 = { x = "val1" }
    local table2 = { y = "val2" }
    local table3 = { z = "val3" }
    setmetatable(table1, {__index = table2, __newindex = table3})

    print(table1.y)
    print(rawget(table1, "y"))

    table1.newN = "newN"
    print(table1.newN)
    print(table3.newN)

    rawset(table1, "newN", "newZ")
    print(table1.newN)

    --输出--
    val2
    nil

    nil
    newN
    newZ

  1. a
网络同步

​ 网络同步的目标时保证多台机器的游戏表现完全一致

  1. 帧同步

    1. 原理:
      • 帧同步的战斗逻辑在客户端;
      • 在帧同步下,服务端只转发操作,不做任何逻辑处理;
      • 客户端按照一定的帧速率(逻辑帧)去上传当前的操作指令,服务端将操作指令广播给所有客户端;
      • 当客户端收到指令后执行本地代码,如果输入的指令一致,计算的过程一致,那么计算的结果肯定是一致的,这样就能保证所有客户端的同步,这就是帧同步。
    2. 缺点:
      • 由于帧同步战斗逻辑都在客户端,服务器没有验证,外挂成本低(加速、透视、自动瞄准、数据修改等);
      • 网络条件较差的客户端会影响其他玩家的游戏体验。(优化方案:乐观帧锁定、渲染与逻辑帧分离、客户端预执行、指令流水线化、操作回滚等);
      • 不同机器浮点数精度问题、容器排序不确定性、RPC时序、随机数值计算不统一。
    3. 乐观帧锁定:
  2. 状态同步

    1. 原理:

      • 状态同步的战斗逻辑在服务端;

      • 在状态同步下,客户端更像是一个服务端数据的表现层;

      • 一般流程:

        • 客户端上传操作到服务器;
        • 服务器收到后计算游戏行为的结果,然后以广播的方式下发游戏中各种状态;
        • 客户端收到状态后再根据状态显示内容。
    2. 缺点:

      • 状态同步的回放实现较为复杂;
      • 延迟过大、客户端性能浪费、服务端压力大;
      • 对带宽的浪费。对于对象少的游戏,可以用快照保存整个游戏的状态发送,但一旦数量多起来,数量的占用就会直线上升。(优化:增量快照同步,协议同步指定数据)
  3. 区别

    属性 状态同步 帧同步
    流量 相对高 相对低
    回放 记录文件大,且实现相对复杂 记录文件小,且相对容易实现
    安全性 服务器实现逻辑,安全性高 逻辑在客户端,防作弊难度高
    服务器压力 逻辑在服务端,压力大
    战斗校验 服务端可以重跑一遍战斗
    网络卡顿表现 瞬移,回位 卡顿
  4. 表现优化

    表现优化用于弱化玩家对延迟的感受。

    1. 插值优化:客户端采用插值,避免位置突变
    2. 客户端预测+回滚
//