Unity光照效果的一些设置
- Windows—Rendering—Light可以打开光照属性的设置,在Environment里可以设置SkyBox、添加雾的效果;
- Hierarchy里右键创建Volume—Global Volume可以调整画面后处理效果;
- Asset里右键Create—Rendering可以添加URP管线Asset,调整管线的设置。
Global Volume的一些效果
- Bloom:过曝、光过量的效果
- Tonemapping:对比度更高、光影更强的颜色
- Color Adjustments:色彩调整
- Chromatic Aberration:鱼眼效果的强度
- Depth Of Field:物理相机的调节
序列化
类的对象是指在内存中的二进制代码。序列化是指将对象以字节流的形式存储到硬盘上,反序列化是指将字节流重新解码成二进制对象存放在内存上。 Unity能够在Hierarchy里拖拽给物体使用的类/脚本,一定是继承自MonoBehavior ; 类里的属性,如果要声明为private,又想在Unity Inspector里查看,就要标注为[SerializeField] ; 类作为属性,添加[System.Serializable] ,可以序列化,在Inspector里编辑。
单例模式
有时候场景中仅允许存在一个类型的对象,比如***Manager,或者背景音乐的对象,如果创建多份对象就会造成声音的重叠播放。 单例对象的创建有两种模式:懒汉式与饿汉式。 懒汉式:在需要时才创建实例对象 饿汉式:在类加载时就创建了实例对象。
public class MouseManager : MonoBehaviour
{
public static MouseManager Instance;
private void Awake()
{
if (Instance != null)
Destroy(gameObject);
Instance = this;
}
}
如上所示,则在别处调用可以写为:
MouseManager.Instance.***
直接写类名调用,因为单例模式独一份,不需要再创建变量赋值。 通过为类设置静态对象,在类加载时就已经在对象内存的BSS区默认初始化Instance为null。然后类对象创建时会执行Awake函数,第一个类对象检测到Instance为null,所以创建Instance实例,第二个乃至第n个对象每次创建时Instance都不为null,于是先把游戏对象删去,再创建一个新的对象实例。以保证类的对象仅有一个。
通常在游戏工程中,把各种Manager都写成单例模式,如果每个Manager都写单例模式比较麻烦,所以写一个泛型的单例模式,然后所有Manager都继承这个单例模式。
UnityEvent
创建一个继承自UnityEvent的类(可以带参数),此类的实例,在启用Invoke() 时可以将参数传递给目标对象。
using UnityEngine.Events;
[System.Serializable]
public class EventVector3 : UnityEvent<Vector3> { }
public class XXX : MonoBehaviour{
public EventVector3 OnMouseClicked;
OnMouseClicked?.Invoke(XXX);
}
如下图所示,注册OnMouseClicked的对象是DogPoly的NavMeshAgent.destination,Event启动时将XXX传递给destination。 UnityEvent是需要拖拽的,如果事件被很多方法注册,就要拖拽多次,不方便,所以可以改成用delegate 事件来做。(如下)
委托与事件event
委托
返回值类型和参数列表相同的函数可以视为同种类型,委托就是同种类型函数指针的链表。 eg:同种类型
void showName(string name)
{
Console.WriteLine("你先输出了名字,参数:"+name);
}
void showTitle(string title)
{
Console.WriteLine("你又输出了主题,参数:"+title);
}
用函数指针链表(也就是委托)来将这两个函数入队列:
delegate void func(string f);
func linkedList;
linkedList += showName;
linkedList += showTitle;
向委托传递参数来执行链表中所有函数: linkedList(“我是参数”)
委托使用+=来添加函数指针,-=来减少函数指针,=来清空函数指针。 调用委托就会执行链表中所有函数指针。
事件(event Action实现
事件就是委托,只不过事件修改了委托的封装性,事件仅可以由拥有这个事件的对象执行。 如下所示,用delegate void System.Action<in T>(T obj) 来声明一个事件OnMouseClicked并使用的例子。
using System;
...
public event Action<Vector3> OnMouseClicked;
...
OnMouseClicked?.Invoke(XXX);
当事件启动时,注册/订阅事件的所有方法都会被传参、调用。 如上所示,当OnMouseClicked事件Invoke的时候,所有订阅此事件的方法会被传值XXX并调用。
事件
事件就是委托,只不过事件修改了委托的封装性,事件仅可以由拥有这个事件的对象执行。
class Program
{
delegate void MyDelegateHanDler(String str);
static event MyDelegateHanDler MyEvent;
static void Main(string[] args)
{
MyEvent += FunA;
MyEvent += FunB;
MyEvent("参数");
MyEvent -= FunB;
MyEvent?.Invoke("参数2");
}
static void FunA(String str)
{
Console.WriteLine("执行方法A:" + str);
}
static void FunB(String str)
{
Console.WriteLine("执行方法B:" + str);
}
}
MouseManager需要的事件包括OnMouseClicked,当我们希望一个游戏对象在单击鼠标的时候干些什么事,我们就可以把该对象干的事情注册到OnMouseClicked事件中,然后由MouseManager执行事件。
IEnumerable和IEnumerator
IEnumerable是可枚举的意思,IEnumerator是枚举器的意思; IEnumerable源码:
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
继承IEnumerable接口的类可以获得一个Enumerator来枚举这个类中集合中的元素。比如List< T >,ArrayList,Dictionary等继承了IEnumerable接口的类。 IEnumerator源码:
public interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
}
添加链接描述
Yeild return
Yield Return是可以让当前协程的进程状态切换到阻塞状态,并且保存当前函数的上下文,然后把CPU交给当前的主进程,这样就转而执行调用方的函数。把一个比较大的任务拆到每一帧里去执行,而不会在程序运行的时候出现卡顿、等待的问题。 yield return 可以接null、一个IEnumerable类型(比如说List)的集合中的一个变量、一个需要分散在各个帧里执行的协程、一个必须等待的时间… eg1:直接返回一个IEnumerable类型(比如说List)的集合中的一个变量 比如打印输出100000个数据,我们需要得到里面的偶数。
private List<int> _numArray;
... _numArray赋值 ...
static public void TestMethod()
{
foreach (var item in GetAllEvenNumber())
{
Console.WriteLine(item);
}
}
static IEnumerable<int> GetAllEvenNumber()
{
foreach (int num in _numArray)
{
if(num % 2 == 0)
{
yield return num;
}
}
yield break;
}
当我们通过yield return,得到一个偶数会立马显示在控制台上,而不是等待把所有偶数都找到之后再一一遍历输出,这样打印数据会让用户感受到有延迟。这样假如我们很多数据,就会一直等这个数据加载好了,才可以拿到这个数据显示给用户。
添加链接描述
eg2: 异步加载中返回需要分布在各个帧里执行的协程 异步场景加载时,使用协程 + yield return实现异步加载。
using UnityEngine.SceneManagement;
void Test()
{
StartCoroutine(LoadOtherScene(otherScene.name));
}
IEnumerator LoadOtherScene(string sceneName)
{
if(SceneManager.GetActiveScene().name != sceneName)
{
yield return SceneManager.LoadSceneAsync(sceneName);
yield break;
}
}
eg3:返回一个必须等待的时间(通过new WaitForSeconds)
IEnumerator LoadSceneAsync(string sceneName)
{
yield return new WaitForSeconds(1);
AsyncOperation asyncOperation = SceneManager.LoadSceneAsync(sceneName);
asyncOperation.completed += OnLoadScene;
}
下个结论:Yield Return 关键字的作用就是退出当前函数,并且会保存当前函数的执行上下文。下次执行这个函数从断点开始继续执行。 但是一般的return result; 之后,之后再调这个函数是会从第一步开始重新执行的,不会记录上次执行的地方。
协程
伴随着另外一些事情同步运行的,通常都会采用协程来做; 需要while的,用协程来做; 加载场景,用协程来做。 Unity协程原理
void Update() {
Debug.Log("001");
StartCoroutine("Demo");
Debug.Log("003");
}
private void LateUpdate() {
Debug.Log("005");
}
IEnumerator Demo() {
Debug.Log("002");
yield return 0;
Debug.Log("004");
}
运行结果: 可以看出,yield return 0 后的内容轮到下一个Unity的生命周期调用,且调用位置是Update() 和LateUpdate() 之后。
Script Object
游戏数值、设置类型可以写作ScriptObject,这样可以直接在asset里生成对应数值的模板,方便添加和管理数值。 eg:
[CreateAssetMenu(fileName="New Data",menuName="Chracters Stats/Data")]
public class ChracterData_SO : ScriptableObject{
public int maxHealth;
public int curHealth;
}
完成以上语句后就可以在asset里Create Chracters Stats下的Data文件,文件名为New Data,然后可以直接在New Data里进行maxHealth和curHealth的赋值。 但是继承自ScriptObject的类不能挂载给物体,所以还要创建继承自MonoBehavior的类,用来get、set读写SO的值。
get、set到属性
有些属性设置为public不是很安全,不想被别的代码随意更改,但是又想控制它的读写权限,就可以用get、set来做,控制它属性的读写。
接口模式A
接口模式有点像C++里的虚继承,不能有自己的实例。接口里面只做方法的定义,不做方法的实现。
public interface IKillable{
void Kill();
}
public interface IDamageable<T>{
void Damage(T damageTaken);
}
接口模式的要求:实现接口的类必须公开声明这个接口中的所有函数方法。
public class Avatar : MonoBehaviour, IKillable, IDamageable<float>{
public void Kill(){...}
public void Damage(float damageTaken){...}
}
Q 为什么有类了还需要接口?
- 类可以继承多个接口,但不能继承多个类;
- 接口用于跨多个不相关的类定义通用功能。
比如wall和car,继承自单个类有点怪,但是继承自IDamageable表示它们都是可破坏的;或者当Player死了之后,所有角色都停止攻击、游戏结束。
通过接口实现观察者模式
观察者模式是指将继承接口的类称为观察者,对其进行收集,比如加入列表,当某个事件发生,让列表中的所有对象都执行接口中的方法(意即发出广播),而这个过程中,这些继承接口的类就被认为订阅 / 成为观察者。 eg:
public interface IEndGameObserver{
void EndNotify();
}
...
List<IEndGameObserver> endGameObservers = new List<IEndGameObserver>();
...
public void NotifyObservers(){
foreach(var observer in endGameObservers){
observer.EndNotify();
}
}
public class EnemyController:MonoBehaviour, IEndGameObserver{
void OnEnable(){
GameManager.Instance.endGameObservers.Add(this);
}
void EndNotify(){
}
}
Timeline制作动画效果
可以用在游戏里播放动画的效果。 Windows—Sequence—Timeline打开Timeline,给物体添加Playable Director组件控制Timeline或者在Timeline窗口里Create一个Timeline文件。 然后可以将物体拖入Timeline左侧,录制模式启动,添加关键帧动画(比如在Transform里Add Key添加,控制相机的位移效果);还可以直接将现成的anim素材拖入轨道,但是一个轨道只能控制一个效果,所以如下所示,为Player的RunForwardBattle轨道点击三个小点添加Override Track,通过关键帧控制Transform。 在代码里using UnityEngine.Playables ,可以通过PlayableDirector 获取场景中的Timeline控制。
ActionTrack
Timeline中添加ActionTrack控制是否启动某个物体。比如可以为EventSystem添加ActionTrack,设置不启动,那么在播放动画的时候EventSystem就不能工作、影响动画了。
Shader效果
遮挡剔除
使用Shader Graph中的 ** Fresnel+Dither(噪点来实现) **角色被遮挡后的视觉效果。 创建好Shader Graph之后,根据此shader创建Material(如名为Occlusion),在管线设置 / Universal Render Pipeline Asset Renderer 中添加Renderer Feature,Material设置为Occlusion,Layer Mask设置为需要实现此效果的Layer,深度测试选Greater(当这些Layer的深度更远,即被遮挡时使用此Renderer Feature渲染); 实现此效果后,这些Layer的物体本身也会被自遮挡,所以再创建一个Renderer Feature,点选Layer Mask,其余保持,则自遮挡就无了。
半角向量指的是光入射方向和观察视线的中间向量
|