前情提要:
会收集自己遇到的问题、在网上碰到的问题、大家分享的问题,尽量用不那么程序化的语言去描述,让大家都可以明白的更快(由于工作量有点大,可能更新的有点慢,不过我会尽力去更新完善的)来张洛琪希美图我们就开始啦!
面向对象:当解决一个问题时,把事物抽象成对象的概念,看这个问题中有哪些对象,给这些对象赋一些属性和方法,让每个对象去执行自己的方法,最终解决问题
面向过程:当解决一个问题时,把事件拆封成一个个函数和数据,然后按照一定的顺序,依次执行完这些方法(过程),等方法执行完毕,事情也解决。
面向过程性能比面向对象要高,但是没有面向对象易于维护、易于复用、易于扩展
- 单一职责原则:一个类的功能要单一,不能太复杂。就像一个人一样,分配的工作不能多,不然虽然很忙碌,但是效率并不高
- 开放封闭原则:一个模块在扩展性方面应该是开放的,而在更改性方面应该是封闭的。例如一个模块,原先只有服务端功能,但是现在要加入客户端功能,那么在设计之初,就应当把服务端和客户端分开,公用的部分抽象出来,从而在不修改服务端的代码前提下,添加客户端代码。
- 里式替换原则:子类应当可以替换父类并出现在父类能出现的任何地方。例如参加年会,公司所有的员工应该都要可以参加抽奖,而不是只让一部分员工是参加抽奖。
- 依赖倒置原则:具体依赖抽象,上层依赖下层。例如B是较A低的模块,但B需要使用A的功能,这时B不能直接使用A中的具体类,而是应当由B来定义一个抽象接口,由A来实现这个抽象接口,B只使用这个抽象接口,从而达到依赖倒置,B也接触了对A的依赖,反过来是A依赖于B定义的抽象接口。
- 接口分离原则:模块之间要通过抽象接口隔离开,而不是通过具体的类强耦合起来
- 封装:将数据和行为相结合,通过行为来进行约束代码,从而增加数据的安全性
- 继承:用来扩展一个类,子类可以继承父类的部分行为和属性,从而便于管理、提高代码的复用
- 多态:一个对象,在不同情形有不同表现形式;子类对象可以赋值给父类型的变量
- 两者定义的方式不同 ;重载方法名相同参数列表不同,重写方法名和参数列表都相同
- 封装、继承、多态所处的位置不同;重载是在同类中,而重写是在父子类中
- 多态的时机不同,重载是在编译时多态,而重写是在运行时多态
通常用在:
字段: 是一种表示与对象或类相关联的变量的成员,字段的声明用于引入一个或多个给定类型的字段。
属性: 是对字段的封装,将字段和访问字段的方法组合在一起,提供灵活的机制来读取、编写或计算私有字段的值。并且属性有自己的名称,包含Get访问器和set访问器
例如:
pulic class User
{
private string _name;//_name为字段
public string Name //Name为属性,它含有代码块
{
get
{
return _name;//读取(返回_name值)
}
set
{
_name = value;//为_name赋值
}
}
如上,属性以灵活的方式实现了对私有字段的访问,是字段的自然扩展,一个属性总是与某个字段相关联,字段能干的,属性一定能干,属性能干的,字段不一定干的了;为了实现对字段的封装,保证字段的安全性,产生了属性,其本质是方法,暴露在外,可以对私有字段进行读写,以此提供对类中字段的保护,字段中存储数据更安全。
------参考原文链接:字段和属性
- 值类型存储在内存栈中,超过作用域自动清理,在作用域结束后会被清理,而引用类型存储在内存堆中,由GC来自动释放,
- 值类型转换成引用类型(装箱)要经历分配内存和拷贝数据
- 引用类型转换成值类型(拆箱)要首先找到属于值类型的地址,再把引用对象的值拷贝到内存栈中的值实例中
- 值类型存取速度快,引用类型存取速度慢
- 值类型表示实际的数据,引用类型表示存储再内存堆中的数据的指针和引用
- 值类型的基类是ValueType,引用类型的基类是Object
- 值类型在内存管理方面有更好的效率,且不支持多态,适合用作存储数据的载体
引用类型支持多态,适合用于定义应用程序的行为- 值类型有:byte、short、int、long、double、decimal、char、bool、struct
引用类型有:array、class、interface、delegate、Object、string
接口:
- 契约式设计,是开发的,谁都可以去实现,但是必须要遵守接口的约定;例如所有游戏都必须会有主角给玩家进行操作,这种必须规范
- 是约束类应该具有的功能集合,从而便于类的扩展和管理
- 接口不能实例化
- 接口关注于行为,可以多继承
- 接口的设计目的,是对类的行为进行约束(提供一种机制,可以强制要求不同的类具有相同的行为,只约束了行为的有无,但是不对如何去实现行为进行约束)
- 通常用于多人协同
抽象类:
- 对同类事物相对具体的抽象,例如我会玩游戏,你如果继承了我,你也必须会玩游戏,但是我不管你玩什么类型游戏;
- 抽象类不能实例化,但是可以间接实例化
- 抽象类是概念的抽象,只能单继承
- 抽象类的设计目的,是代码复用(当不同的类具有某些相同的行为,且其中一部分行为的实现方法一致时,可以让这些类都派生于一个抽象类。在这个抽象类中实现了公用的行为,从而可以避免让所有的子类来实现该行为,从而让子类可以自己去实现抽象类中没有的部分)
- 一个类可以有多个接口,但是只能继承一个类
委托是一个类,它定义了方法的类型,使得可以将多个方法赋给同一个委托变量,当调用这个变量时,将依次调用其绑定的方法
一般调用方法,我们称为直接调用方法,而委托可以间接调用方法,也就是委托封装了一个或多个方法,方法的类型必须与委托的类型兼容;同时委托也可以当做方法的参数,传递到某个方法中去,当使用这个传进来的委托时,就会间接的去调用委托封装的方法,从而形成动态的调用方法的代码,并且降低了代码的耦合性。
using UnityEngine;
//声明委托,可以在类的外面,也可以在类的内部
public delegate X ADelegate<in T, out X>(T t);//有返回值的委托 对应Func委托 in为输入 out输出
public delegate void BDelegate<in T>(T t);//无返回值的委托 对应Action委托
public class DelegateTest : MonoBehaviour
{
public ADelegate<int, string> MyTest;
[ContextMenu("Exe")]
private void Start()
{
MyTest = IntNumber;
TestDelegate(MyTest);//输出 返回值:100
//目标实例(当目方法为静态时,目标实例为null)
Debug.Log(MyTest.Target); //输出 DelegateTest
//实例方法
Debug.Log(MyTest.Method); //输出 System.String IntNumber(Int32)
//(返回值类型)(目标方法)(方法参数)
}
public string IntNumber(int a) => (a * a).ToString();
public void TestDelegate(ADelegate<int, string> aDelegate) => Debug.LogFormat("返回值:{0}", aDelegate?.Invoke(10));
}
using System;
using UnityEngine;
public class DelegateTest : MonoBehaviour
{
[ContextMenu("Exe")]
private void Start()
{
TestDelegate(Add);
TestDelegate(Sub);
//-----------委托与接口类似,委托能实现的,接口也能实现,但又有所不同
TestInterface(new Add());
TestInterface(new Sub());
/*
* 对于这个例子这种情况,更适合使用委托,因为接口而言,要多次实现接口,过于麻烦。
*/
}
public int Add(int num) => num + num;
public int Sub(int num) => num - num;
public void TestDelegate(Func<int ,int> _func) => Debug.LogFormat("返回值:{0}", _func?.Invoke(10));
public void TestInterface(ICalcNumber calc) => Debug.LogFormat("返回值:{0}", calc.Calc(10));
}
public interface ICalcNumber
{
public int Calc(int num);
}
class Add : ICalcNumber
{
public int Calc(int num) => num + num;
}
class Sub : ICalcNumber
{
public int Calc(int num) => num - num;
}
事件,他可以让一个类或对象去通知其他类、对象,并做出相应的反应;是封装了委托类型的变量,使得:在类的内部,不管是public和protected,总是private的。在类的外部,注册"+=“和注销”-="的访问限制夫和在声明事件时使用的访问符一致。其主要目的就是为了防止订阅者之间相互干扰。
using System;
using UnityEngine;
///
///
/// * Writer:June
/// *
/// * Data:2021.5.12
/// *
/// * Function:事件例子=====>顾客点单
/// *
/// * Remarks:
///
///
/// 事件模型的五个组成部分
/// 事件的拥有者: 类
/// 事件: event关键字修饰
/// 事件的响应者: 类
/// 事件处理器: 方法-受到约束的方法
/// 事件的订阅关系: +=
///
///
/* 用于事件约束的委托类型,签名应该遵循:事件名+EventHandler
* (当别人看到这个后缀之后,就知道,这个委托是用来约束事件处理器的,不会用于做别的事!!!)
*/
public delegate void OrderEventHandler(Customer _customer, OrderEventArgs _oe);
public class EventEx : MonoBehaviour
{
//顾客类实例化
Customer customer = new Customer();
//服务员类实例化
Waiter waiter = new Waiter();
private void Start()
{
customer.OnOrder += waiter.TakeAction;
customer.Order();
customer.PayTheBill();
}
}
public class Customer
{
public float Bill { get; set; }
public void PayTheBill() => Debug.Log("我应该支付:" + Bill);
#region 事件的完整声明格式
//private OrderEventHandler orderEventHandler;
//public event OrderEventHandler OnOrder
//{
// add { orderEventHandler += value; }//事件添加器
// remove { orderEventHandler -= value; }//事件移除器
//}
//public void Order()
//{
// if (orderEventHandler != null)
// {
// OrderEventArgs orderEventArgs = new OrderEventArgs
// {
// CoffeeName = "摩卡",
// CoffeeSize = "小",
// CoffeePrice = 28
// };
// orderEventHandler(this, orderEventArgs);
// OrderEventArgs orderEventArgs1 = new OrderEventArgs
// {
// CoffeeName = "摩卡",
// CoffeeSize = "小",
// CoffeePrice = 28
// };
// orderEventHandler(this, orderEventArgs1);
// }
//}
#endregion
#region 事件的简略声明格式
public event OrderEventHandler OnOrder;
public void Order()
{
//语法糖衣
if (OnOrder != null)
{
OrderEventArgs orderEventArgs = new OrderEventArgs
{
CoffeeName = "摩卡",
CoffeeSize = "小",
CoffeePrice = 28
};
OnOrder(this, orderEventArgs);
OrderEventArgs orderEventArgs1 = new OrderEventArgs
{
CoffeeName = "摩卡",
CoffeeSize = "小",
CoffeePrice = 28
};
OnOrder(this, orderEventArgs1);
}
}
#endregion
}
public class OrderEventArgs : EventArgs
{
public string CoffeeName { get; set; }
public string CoffeeSize { get; set; }
public float CoffeePrice { get; set; }
}
public class Waiter
{
//事件处理器
internal void TakeAction(Customer _customer, OrderEventArgs _oe)
{
float finaPrice = 0;
switch (_oe.CoffeeSize)
{
case "小":
finaPrice = _oe.CoffeePrice;
break;
case "中":
finaPrice = _oe.CoffeePrice + 3;
break;
case "大":
finaPrice = _oe.CoffeePrice + 6;
break;
default:
break;
}
_customer.Bill += finaPrice;
}
}
简而言之,委托是事件的底层基础,事件是委托的上层建筑,类似于字段和属性之间的关系,属性包装着字段,通过一系列的逻辑来保护字段。事件也是如此,起到保护委托类型,以免被外界滥用。他只能通过+=、-=来添加和移除事件处理器,不能直接的外部进行访问和调用
------参考原文链接:事件和委托详解_1
------参考原文链接:事件和委托详解_2
一个用来代替委托实例的匿名方法(本质是一个方法,只是没有名字,任何使用委托变量的地方都可以使用匿名方法赋值),从而让代码更加简洁
被Lambda表达式引用的外部变量叫做被捕获的变量;捕获了外部变量的Lambda表达式叫做闭包;被捕获的变量是在委托被实际调用的时候才被计算,而不是在捕获的时候
//不使用Lambda表达式时的一个点击事件
public class button : MonoBehaviour
{
public Button btn;
void Start ()
{
btn.onClick.AddListener(btnClick);
}
void btnClick()
{
Debug.Log("按钮被点击了!");
}
}
-------------------------------------------------------------------
-------------------------------------------------------------------
//使用Lambda表达式时的一个点击事件
public class button : MonoBehaviour
{
public Button btn;
void Start ()
{
//无参匿名函数
btn.onClick.AddListener(()=>
{
Debug.Log("按钮被点击了");
}
}
}
public class LambdaTest : MonoBehaviour
{
public delegate int MyDelegate(int t);
[ContextMenu("Exe")]
private void Start()
{
/** 实际上,编译器会通过编写一个私有方法来解析这个lambda表达式
* 然后把表达式的代码移动到这个方法里
**/
MyDelegate my = (x) => { return x * x; };//在只有一个可推测类型的参数时,可以省略小括号
MyDelegate my1 = (x) => x * x;
int num = my(2);
Debug.Log(num);
}
}
For循环通过下标,对循环中的代码反复执行,功能强大,可以通过Index(索引)去取得元素。
Foreach循环从头到尾,对集合中的对象进行遍历。在遍历的时候,会锁定集合的对象,期间是只读的,不能对其中的元素进行修改。
Foreach相对于For循环,其运行时的效率要低于For循环,内存开销也比For循环要高些。但是,在处理一些不确定循环次数的循环或者循环次数需要计算的情况时,使用Foreach更方便些。
Awake->OnEnable->Start->FixedUpdate->Update->LateUpdate->OnDisable->OnDestory
- Awake 一般用来实现单例,场景开始时每个对象调用一次,如果对象处于非活动状态,则在激活状态后才会调用Awake
- OnEnable 在启用对象后立即调用此函数,在场景中有可能会重复调用(反复隐藏和激活)
- start
- FixedUpdate通常用来进行物理逻辑的更新,其频率常常超过Update。在里面进行应用运动计算时,无需将值乘以Time.deltaTime.因为FixedUpdate的调用基于可靠的计时器(独立于帧率)
- Update 每帧调用一次。用于帧更新的主要函数
- LateUpdate通常用来进行摄像机的位置更新,以确保角色在摄像机跟踪其位置之前已经完全移动
- OnDisable 在隐藏对象后立即调用此函数,在场景中有可能会重复调用(反复隐藏和激活)
一旦选择±90°作为Picth(倾斜)角,就会导致第一次旋转和第三次旋转等价,从而表示系统被限制在只能绕竖直轴去旋转,丢失了一个表现维度;
解决方法:用四元数可以避免万向锁的产生,缺点是耗费一定的内存,但是目标可以任意旋转,自由度高
- 关键帧动画: 在动画序列的关键帧中记录各个顶点的原位置及其改变量,然后通过插值运算实现动画效果,角色动画较为真实
- 骨骼动画: 骨骼按照角色的特点组合成一定的层次结构,有关节相连,可做相对运动,皮肤作为单一的网格蒙在骨骼之外,用来决定角色的外观
-关节动画: 把角色分为若干部分,每一部分对应一个网格模型,部分的动画连接成一个整体的动画,角色比较灵活
这里是引用
游戏界面中可以看到很多摄像机的混合
区域光源:在实时光照模式下是无效的,仅在烘培光照模式下有用
点光源:模拟电灯泡的照射效果
平行光源:模拟太阳光效果
聚光灯:模拟聚光灯效果
在三维软件中打好灯光,然后渲染把场景中各表面的光照输出到贴图上,最后通过引擎贴到场景中,从而使得物体有了光照的效果
本影:
半影:
这时可以使用对象池,那什么是对象池呢?
- 对象池中,存放着需要被反复调用资源的一个空间,例如射击游戏中的子弹,大量的Instantiate()来创建新的对象和Destory()销毁对象,对内存消耗极大。
- 原理:一开始创建一些物体,并将其隐藏起来,对象池就是这些物体的集合,可以是UI中的无限循环列表元素,也可以是物体,当需要使用时,再将其激活使用,超过使用范围时再将其隐藏起来,通常来说,一个对象池存储的都是一类对象(也可以用字典来创建对象池,这样能指定每个对象池存储对象的类型,同时也可以用tag去访问相应的对象池)
using System.Collections.Generic;
using UnityEngine;
public class BulletsPool : MonoBehaviour
{
//单例,提供全局访问节点,并且保证只有一个子弹对象池存在
public static BulletsPool bulletsPoolInstance;
public GameObject bulletObj;//子弹实例
public int pooledAmount = 50;//预存储对象池的大小
public bool lockPoolSize = false;//是否可以动态扩容
private List<GameObject> pooledObjects;//用一个列表来存储对象,即对象池
private int currentIndex = 0;//当前的索引
void Awake()
{
bulletsPoolInstance = this;
}
void Start()
{
pooledObjects = new List<GameObject>();//实例化
for (int i = 0; i < pooledAmount; ++i)//生成对象池并填充
{
GameObject obj = Instantiate(bulletObj);
obj.transform.SetParent(gameObject.transform);
obj.SetActive(false);
pooledObjects.Add(obj);
}
}
public GameObject GetPooledObject()
{
for (int i = 0; i < pooledObjects.Count; ++i)
{
//这里其实是一个贪心算法,当一个对象被激活时,
//如要激活下一个对象,索引是从上一个对象开始,而不是从头开始查找
int temI = (currentIndex + i) % pooledObjects.Count;
if (!pooledObjects[temI].activeInHierarchy)
{
currentIndex = (temI + 1) % pooledObjects.Count;
return pooledObjects[temI];
}
}
//如果没有固定对象池大小,则往列表中添加预制件
//(因为列表是可以动态扩容的,所以不用担心会出现越界)
if (!lockPoolSize)
{
GameObject obj = Instantiate(bulletObj);
pooledObjects.Add(obj);
return obj;
}
return null;
}
}
/*用法也很简单,获取对象池中的预制件,如果不为空,则将其激活,并将其的坐标放到开火点,并发射出去,
当其撞到敌人或者墙时,播放相应的粒子特效,然后将其隐藏起来,用来下次激活(这里没有列出代码,原理和激活相同)*/
GameObject bullet=BulletsPool.bulletsPoolInstance.GetPooledObject();
if (bullet != null)
{
bullet.SetActive(true);
bullet.transform.position = firePoint.position;
bullet.transform.rotation = Quaternion.Euler(new Vector3(0, 0, Random.Range(-15, 20)));
bullet.GetComponent<Bullet>().SetDirection(Mathf.Sign(transform.localScale.x));
}
这里是引用
修改Material会重新生成一个新的Material到内存中,然后修改后的物体就使用这个新的材质,但是,在销毁物体时,需要手动去销毁该Material,不然会一种存在于内存中。
修改SharedMaterial将改变所有使用这个材质的物体的外观,并且也会改变存储在工程中的材质设置
设计模式是一套被反复使用、多数人知晓、经过分类编目、代码设计经验的总结。
- 开闭原则:对扩展开放,对修改
- 单一职责原则:一个类只负责一个功能领域中的相应职责
- 里氏转换原则:所有引用基类的地方必须能透明的去使用其子类的对象
- 依赖倒转原则:依赖于抽象,不能依赖于具体实现
- 接口依赖原则:类之间的依赖关系应该建立在最小的接口上
- 合成/聚合复用原则:尽量使用合成/聚成,而不是通过继承来达到复用的目的
- 最少知识原则(迪米特法则):一个软件应当尽可能少的和其他实体发生相互作用
在整个游戏生命周期中,有很多对象从始至终有且只有一个,而着唯一的实例也只需要生成一次,并提供了一种访问其唯一的对象的方法,可以直接访问,不需要实例化该类的对象,直到游戏结束才会被销毁。所以,单例模式一般应用于管理器类或者是一些需要持久化存在的对象。
public sealed class Singleton
{
private static Singleton instance;
public static Singleton Instance()
{
get
{
if(instance==null)
instance=new Singleton();
return instance;
}
}
}
------参考原文链接:单例模式的使用
单例模式也分为两种模式
- 饿汉模式:在类加载时就生成该单例对象
优点:简单方便;线程安全,调用时反应速度快
缺点:会降低启动速度;不关程序是否使用,都会创建该单例对象
应用场景:单例对象功能简单,占用内存小,需要频繁使用的时候
public class HungrySingLeton
{
private static HungrySingLeton instance;
private HungrySingLeton()
{
instance = new HungrySingLeton();
}
public static HungrySingLeton Instace
{
get{return instance;}
}
}
- 懒汉模式:第一次调用时才创建该单例对象
优点:在需要时创建,利用率高;提高启动速度
缺点:多线程不安全,可能会创建多个实例
应用场景:单例对象功能复杂,内存占用大,需要快速启动
public class LazySingleton
{
private static LazySingleton _instance;
public static LazySingleton Instace
{
get
{
if (_instance == null)
{
_instance = new LazySingleton();
}
return _instance;
}
}
}
------参考原文链接:引用代码链接
- 栈:
先进后出,特殊的线性表,只能在表的一个固定端去进行插入和删除- 堆:
特殊的树形数据结构- 队列:
先进先出,特殊的线性表,只能在一端进行插入,另一端进行删除- 数组:
底层是线性表,使用前需声明长度,不安全,是个存储在连续内存位置的相同类型数据项的集合- 列表
底层是泛型数组,可动态扩容,泛型安全- 链表
数据元素按照链式存储结构进行存储的数据结构,内存位置是不连续的。内部有数据元素为节点,每个节点包含两个字段,分别是数据字段和链接字段,通过一个指针链接到它的相邻节点。- 字典
内部使用哈希表作为存储结构,是包含键和值集合的抽象数据类型,每个键都有一个相关联的值;如果试图找到一个不存在的值,则会抛出异常。- 哈希表
不定长的二进制数据通过哈希函数印射到一个较短的二进制数集- 树
包含一个和多个数据节点的集合,其中一个节点被指定为树的根,其余节点被称为根的子节点- 图
可以把图视为循环树,其中每个节点保持他们之间的任何复杂关系,而不是具有父子关系
这里是引用
这里是引用
这里是引用
这里是引用
这里是引用
这里是引用
这里是引用
这里是引用