游戏物体生命周期
- 场景中的游戏物体通过脚本来控制其具体行为。
- 生命周期流程图:点击跳转
- 脚本里重点事件函数:
- Awake:游戏物体实例化后并处于激活状态时调用,即使脚本组件没有激活也会调用,而且总是在Start()函数之前调用
- OnEnable:游戏物体与脚本组件激活时调用(会反复触发)
- Start:游戏物体与脚本组件处于激活状态,在Update()运行前调用(只调用一次,当物体关闭激活状态,再打开时不会反复触发)
- FixedUpdate:根据设定的帧率运行(帧率固定,主要用于刚体运算,存在平衡机制去约束)
- Update:每帧调用一次(帧率不固定,与硬件性能以及逻辑复杂度相关)
- LateUpdate:每帧调用一次,在Update()之后调用
- OnDisable:游戏物体/脚本组件关闭激活时调用(会反复触发)
- OnDestroy:当游戏物体销毁时调用
- 注意:在一个场景加载以后,场景当中的每个物体都会遵循上述原则去运行物体上挂载脚本的函数
核心要素
- OnEnable/OnDisable可以反复触发
- Awake/Start始终只触发一次
- FixedUpdate运行帧率由:Edit-Project Settings-Time-Fixed Timestep的值决定 注:每秒运行帧率相等不代表运行时间相等。可通过Time.realtimeSinceStartup测试出来每次FixedUpdate()调用时间间隔是不一样的。
- 为保证FixedUpate的固定帧率,通过设置Edit-Project Settings-Time Maximum Allowed Timestep来实现平衡机制,限定每次FixedUpdate()运行的最长时间。实现效果与性能的平衡。
帧循环/主线程概念
- 为了保证数据安全,Unity核心的游戏逻辑全部都是在一个线程里完成。也就是我们常说的Unity主线程。 在主线程上运行一次完整的游戏逻辑帧我们称之为完成了一次帧循环,也称为主循环。
- 注意几点:
- 一个逻辑帧我们通常是指一次包含了以Update()为主的循环调用过程。因为我们游戏的逻辑大部分都是运行在Update()里面。
- 一个逻辑帧里面可能包含了多次FixedUpdate()的调用。
- Unity中是可以使用多线程的,但需要注意在Unity里一些关键性的数据在其它线程是不能访问修改的
private void Start() {
ThreadStart threadStart = new ThreadStart(ThreadMain);
Thread thread = new Thread(threadStart);
thread.Start();
Debug.Log("UnityMain线程ID:" + Thread.CurrentThread.ManagedThreadId.ToString());
}
void ThreadMain() {
Debug.Log("New线程ID:" + Thread.CurrentThread.ManagedThreadId.ToString());
//运行会报错:get_transform can only be called from the main thread.
Debug.Log(transform.gameObject.name);
}
- Unity限制使用多线程的原因:
- 保证数据安全
- 降低编程难度
- 为何还能保持高效率的运行?
- Unity在底层实现了线程池,引擎底层来实现一些可使用多线程处理的任务
协程的常规使用1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameRoot : MonoBehaviour
{
private void Start()
{
//StartCoroutine(FuncA());//可以传很多参数
StartCoroutine("FuncA");//只能传一个参数,用逗号链接
}
IEnumerator FuncA()
{
Debug.Log("Log1:");
yield return new WaitForSeconds(2f);
Debug.Log("Log2:");
}
}
协程的常规使用2
两种协程启动和关闭的方法需要匹配,不然可能出现关闭不了的情况
StartCoroutine("FuncA");//只能传一个参数,用逗号链接
StopCoroutine("FuncA");
Coroutine cot;
cot = StartCoroutine(FuncA());//可以传很多参数
StopCoroutine(cot);
协程语法怎样理解:
1.协程,顾名思义,就是“协同程序”,用来实现协作。 2.比如在游戏中需要等待1秒后执行一个操作。我们不能让游戏主线程卡死等1秒,而是开启一个协程进行协作,协程同样是由主线程来驱动(所以说它不是线程),会在每一帧去检测它是否已经达到了完成的条件。比如条件是1秒后执行一个操作,那么在1秒后主线程对它检测时。条件满足,于是就执行先前设定好的相关操作。
为什么要有协程?
- Uniy核心逻辑是单线程,为了开发便利,统一生命周期管理,不需要考虑锁的问题。
- 一些导步操作非常耗时,比如资源加载,如果让用户去使用多线程开发,整个开发逻辑会非常复杂。而通过协程封装起来,方便用户使用。并且对于一些耗废时间的操作,Unity会在引擎底层通过多线程去完成,而协程则通过不断的访问这个操作的状态判断加载是否完成。
启动协程的方法:
- StartCoroutine(IEnumerator routine)
- StartCoroutine(string routine)或StartCoroutine(string routine,object value) 通过传入函数名字的字符串启动协程的性能开销要更高,但这种方式启动的协程可以通过StopCoroutine(string methondname)来终止,这种方式还有一个限制就是只能传递一个参数。
协程终止的方法
- StopCoroutine(string methondname)只能终止通过字符串名字启动的协程
- StopCoroutine(Coroutine coroutine)
- StopAllCoroutines()终止所有协程
- 在协程内部终止可以使用
yeild break;
- 还有一种方法是直接把物体的active属性设置为false,这里是一个大坑,容易引发bug
(Plane备注:以上都是针对当前这个MonoBehaviour中的协同程序而言)
协程的一些特性
- 在一个协程中可以启动另一个协程
- 一个协程可以暂停另一个协程
- 两个协程也可以同时并行工作
- 协程在执行过程中可以通过yield在任何时间点暂停,yeild返回的值决定了协程何时恢复执行
- 在多个游戏帧循环中进行操作时,协程表现十分出色,几乎没有额外性能开销
- StartCoroutine()函数总是会立即返回,不管你yeild返回的值是什么
- 多个协程结束时的顺序是无法保证与其启动顺序一致的,即使他们是在同一帧当中结束
关于yeild返回的值
- 可以是null,数字 ,字符串,布尔值甚至表达式,函数,嵌套协程。
- 当return是一个函数调用,赋值表达式,嵌套协程时,会直接调用这个函数或表达式。
- 协程一般情况下是在Update调用完成之后运行,在yeild后面的条件满足之前,协程会一直挂起在那里。
- 不同的yeild返回类型对应了不同的条件:
- null,数字,字符串:会在下一帧所有Update()函数运行之后继续运行
- WaitForSeconds:延迟指定的时间之后,在所有的Update()函数运行之后继续运行
- WaitForFixedUpdate:在所有FixedUpdate() 函数运行之后继续运行。(注意这里不是指下一帧,因为FixedUpdate()的帧率和Update()是不一样的)
- WWWW:当WWW资源加载成功以后继续运行。(其它的异步资源加载函数也是一样)
Unity中定时器实现
搭建测试环境
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//功能:计时系统
public class TimerSystem : MonoBehaviour
{
public static TimerSystem Instance;
private void Awake()
{
Instance = this;
}
public void AddTimerTask()
{
Debug.Log("Add Time Task");
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//功能:启动入口
public class GameRoot : MonoBehaviour
{
private void Start()
{
TimerSystem.Instance.AddTimerTask();
}
}
初始化脚本顺序
修改GameRoot和TimerSystem脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//功能:启动入口
public class GameRoot : MonoBehaviour
{
private void Start()
{
Debug.Log("Game Start...");
TimerSystem timerSystem = GetComponent<TimerSystem>();
timerSystem.InitSys();
}
public void ClickAddBtn()
{
TimerSystem.Instance.AddTimerTask();
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//功能:计时系统
//开发目标:
//支持时间定时,帧定时
//定时任务可循环,可取消,可替换
//使用简单,调用方便
public class TimerSystem : MonoBehaviour
{
public static TimerSystem Instance;
public void InitSys()
{
Instance = this;
Debug.Log("TimeSys Init Done.");
}
private void Update()
{
//遍历检测任务是否达到条件 TODO
}
public void AddTimerTask()
{
Debug.Log("Add Time Task");
}
}
基础定时功能实现
创建一个PETimeTask类
//定时任务的数据类
using System;
class PETimeTask
{
public float destTime;
public Action callback;
}
修改TimerSystem里面的代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
//功能:计时系统
//开发目标:
//支持时间定时,帧定时
//定时任务可循环,可取消,可替换
//使用简单,调用方便
public class TimerSystem : MonoBehaviour
{
public static TimerSystem Instance;
private List<PETimeTask> taskTimeList = new List<PETimeTask>();
public void InitSys()
{
Instance = this;
Debug.Log("TimeSys Init Done.");
}
private void Update()
{
//遍历检测任务是否达到条件
for(int index = 0; index < taskTimeList.Count; index++)
{
PETimeTask task = taskTimeList[index];
if(Time.realtimeSinceStartup < task.destTime)
{
continue;
}
else
{
Action cb = task.callback;
if(cb != null)
{
cb();
}
//移除已经完成的任务
taskTimeList.RemoveAt(index);
index--;
}
}
}
public void AddTimerTask(Action callback, float delay)
{
float destTime = Time.realtimeSinceStartup + delay;
PETimeTask timeTask = new PETimeTask();
timeTask.destTime = destTime;
timeTask.callback = callback;
taskTimeList.Add(timeTask);
}
}
在GameRoot里面进行调用
public void ClickAddBtn()
{
Debug.Log("Add Time Task");
TimerSystem.Instance.AddTimerTask(FuncA,2.0f);
}
void FuncA()
{
Debug.Log("Delay Log");
}
增加临时缓存列表
新增一个tempTaskList用来进行缓存,现将所有的任务放进缓存,然后在从缓存放进taskTimeList。
private List<PETimeTask> tempTimeList = new List<PETimeTask>();//缓存列表
在Update里添加下面代码
//加入缓存区中的定时任务
for(int index = 0; index < tempTimeList.Count; index++)
{
taskTimeList.Add(tempTimeList[index]);
}
tempTimeList.Clear();
增加时间单位设置功能
默认PETimeTask里面的destTime为毫秒。添加几个枚举类型来表示其他时间单位
public enum PETimeUnit
{
Millisecond,
Second,
Minute,
Hour,
Day
}
更新TimeSystem里面的方法,添加时间转变的的方法
//首先要换算单位
if (timeUnit != PETimeUnit.Millisecond)
{
switch (timeUnit)
{
case PETimeUnit.Second:
delay = delay * 1000;
break;
case PETimeUnit.Minute:
delay = delay * 1000 * 60;
break;
case PETimeUnit.Hour:
delay = delay * 1000 * 60 * 60;
break;
case PETimeUnit.Day:
delay = delay * 1000 * 60 * 60 * 24;
break;
default:
Debug.Log("Add Task TimeUnit Type Error...");
break;
}
}
增加任务循环功能
修改PETimeTask类和TimeSystem
//定时任务的数据类
using System;
class PETimeTask
{
public float destTime;//记录开始运行的时间 单位是毫秒
public float delay;//延迟的时间
public Action callback;
public int count;//执行次数 0:表示该方法一直执行
public PETimeTask(Action callback,float destTime,float delay,int count)
{
this.callback = callback;
this.destTime = destTime;
this.delay = delay;
this.count = count;
}
}
public enum PETimeUnit
{
Millisecond,
Second,
Minute,
Hour,
Day
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
//功能:计时系统
//开发目标:
//支持时间定时,帧定时
//定时任务可循环,可取消,可替换
//使用简单,调用方便
public class TimerSystem : MonoBehaviour
{
public static TimerSystem Instance;
private List<PETimeTask> tempTimeList = new List<PETimeTask>();//缓存列表
private List<PETimeTask> taskTimeList = new List<PETimeTask>();
public void InitSys()
{
Instance = this;
Debug.Log("TimeSys Init Done.");
}
private void Update()
{
//加入缓存区中的定时任务
for(int index = 0; index < tempTimeList.Count; index++)
{
taskTimeList.Add(tempTimeList[index]);
}
tempTimeList.Clear();
//遍历检测任务是否达到条件
for(int index = 0; index < taskTimeList.Count; index++)
{
PETimeTask task = taskTimeList[index];
//当实际时间小于应该运行的时间,说明程序已经运行过,跳过
if(Time.realtimeSinceStartup * 1000 < task.destTime)
{
continue;
}
else
{
Action cb = task.callback;
if(cb != null)
{
cb();
}
//移除已经完成的任务
if(task.count == 1)
{
taskTimeList.RemoveAt(index);
index--;
}
else
{
if (task.count != 0)
task.count -= 1;
task.destTime += task.delay;
}
}
}
}
public void AddTimerTask(Action callback, float delay, PETimeUnit timeUnit = PETimeUnit.Millisecond,int count = 1)
{
//首先要换算单位
if (timeUnit != PETimeUnit.Millisecond)
{
switch (timeUnit)
{
case PETimeUnit.Second:
delay = delay * 1000;
break;
case PETimeUnit.Minute:
delay = delay * 1000 * 60;
break;
case PETimeUnit.Hour:
delay = delay * 1000 * 60 * 60;
break;
case PETimeUnit.Day:
delay = delay * 1000 * 60 * 60 * 24;
break;
default:
Debug.Log("Add Task TimeUnit Type Error...");
break;
}
}
float destTime = Time.realtimeSinceStartup * 1000 + delay;
PETimeTask timeTask = new PETimeTask(callback, destTime,delay, count);
tempTimeList.Add(timeTask);
}
}
生成定时任务全局ID
如何扩展取消定时任务?
1.生成全局唯一ID
2.通过ID索引操作任务
给PETimeTask添加id
public int tid;
修改TimeSystem中的部分代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
//功能:计时系统
//开发目标:
//支持时间定时,帧定时
//定时任务可循环,可取消,可替换
//使用简单,调用方便
public class TimerSystem : MonoBehaviour
{
public static TimerSystem Instance;
private static readonly string obj = "lock";
private int tid;
private List<int> tidList = new List<int>();
private List<PETimeTask> tempTimeList = new List<PETimeTask>();//缓存列表
private List<PETimeTask> taskTimeList = new List<PETimeTask>();
public void InitSys()
{
Instance = this;
Debug.Log("TimeSys Init Done.");
}
private void Update()
{
//加入缓存区中的定时任务
for(int index = 0; index < tempTimeList.Count; index++)
{
taskTimeList.Add(tempTimeList[index]);
}
tempTimeList.Clear();
//遍历检测任务是否达到条件
for(int index = 0; index < taskTimeList.Count; index++)
{
PETimeTask task = taskTimeList[index];
//当实际时间小于应该运行的时间,说明程序已经运行过,跳过
if(Time.realtimeSinceStartup * 1000 < task.destTime)
{
continue;
}
else
{
Action cb = task.callback;
if(cb != null)
{
cb();
}
//移除已经完成的任务
if(task.count == 1)
{
taskTimeList.RemoveAt(index);
index--;
}
else
{
if (task.count != 0)
task.count -= 1;
task.destTime += task.delay;
}
}
}
}
public void AddTimerTask(Action callback, float delay, PETimeUnit timeUnit = PETimeUnit.Millisecond,int count = 1)
{
//首先要换算单位
if (timeUnit != PETimeUnit.Millisecond)
{
switch (timeUnit)
{
case PETimeUnit.Second:
delay = delay * 1000;
break;
case PETimeUnit.Minute:
delay = delay * 1000 * 60;
break;
case PETimeUnit.Hour:
delay = delay * 1000 * 60 * 60;
break;
case PETimeUnit.Day:
delay = delay * 1000 * 60 * 60 * 24;
break;
default:
Debug.Log("Add Task TimeUnit Type Error...");
break;
}
}
int tid = GetTid();
float destTime = Time.realtimeSinceStartup * 1000 + delay;
PETimeTask timeTask = new PETimeTask(tid,callback, destTime,delay, count);
tempTimeList.Add(timeTask);
tidList.Add(tid);
}
private int GetTid()
{
lock (obj)
{
tid += 1;
//安全代码,以防万一
while (true)
{
if (tid == int.MaxValue)
{
tid = 0;
}
bool used = false;
for(int i = 0; i < tidList.Count; i++)
{
if(tid == tidList[i])
{
used = true;
break;
}
}
if (!used)
{
break;
}
else
{
tid += 1;
}
}
}
return tid;
}
}
增加任务删除功能
修改GameRoot里面的方法
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//功能:启动入口
public class GameRoot : MonoBehaviour
{
private void Start()
{
Debug.Log("Game Start...");
TimerSystem timerSystem = GetComponent<TimerSystem>();
timerSystem.InitSys();
}
int tid;
public void ClickAddBtn()
{
Debug.Log("Add Time Task");
tid = TimerSystem.Instance.AddTimerTask(FuncA,1000,PETimeUnit.Millisecond,0);
}
void FuncA()
{
Debug.Log("tid:" + tid);
}
public void ClickDelBtn()
{
Debug.Log("Del Time Task");
bool ret = TimerSystem.Instance.DeleteTimeTask(tid);
Debug.Log("Del Time Task:" + ret);
}
}
在TimeSystem里面添加删除的方法
public bool DeleteTimeTask(int tid)
{
bool exist = false;
for(int i = 0; i < taskTimeList.Count; i++)
{
PETimeTask task = taskTimeList[i];
if(task.tid == tid)
{
taskTimeList.RemoveAt(i);
for(int j = 0; j < tidList.Count; j++)
{
if(tidList[j] == tid)
{
tidList.RemoveAt(j);
break;
}
}
}
exist = true;
break;
}
if (!exist)
{
for (int i = 0; i < tempTimeList.Count; i++)
{
PETimeTask task = tempTimeList[i];
if (task.tid == tid)
{
taskTimeList.RemoveAt(i);
for (int j = 0; j < tidList.Count; j++)
{
if (tidList[j] == tid)
{
tidList.RemoveAt(j);
break;
}
}
}
exist = true;
break;
}
}
return exist;
}
增加任务替换功能
GameRoot里面添加下面的代码
public void ClickRepBtn()
{
Debug.Log("Rep Time Task");
bool ret = TimerSystem.Instance.ReplaceTimeTask(tid, FuncB, 2000);
Debug.Log("Rep Time Task:" + ret);
}
void FuncB()
{
Debug.Log("New Task Replace Done." );
}
TimeSystem中添加替换的功能
public bool ReplaceTimeTask(int tid, Action callback, float delay, PETimeUnit timeUnit = PETimeUnit.Millisecond, int count = 1)
{
//首先要换算单位
if (timeUnit != PETimeUnit.Millisecond)
{
switch (timeUnit)
{
case PETimeUnit.Second:
delay = delay * 1000;
break;
case PETimeUnit.Minute:
delay = delay * 1000 * 60;
break;
case PETimeUnit.Hour:
delay = delay * 1000 * 60 * 60;
break;
case PETimeUnit.Day:
delay = delay * 1000 * 60 * 60 * 24;
break;
default:
Debug.Log("Add Task TimeUnit Type Error...");
break;
}
}
float destTime = Time.realtimeSinceStartup * 1000 + delay;
PETimeTask newTask = new PETimeTask(tid, callback, destTime, delay, count);
bool isRep = false;
for (int i = 0; i < taskTimeList.Count; i++)
{
if(taskTimeList[i].tid == tid)
{
taskTimeList[i] = newTask;
isRep = true;
break;
}
}
if (!isRep)
{
for (int i = 0; i < tempTimeList.Count; i++)
{
if (tempTimeList[i].tid == tid)
{
tempTimeList[i] = newTask;
isRep = true;
break;
}
}
}
return isRep;
}
清理定时任务全局ID
对在tidList里重复的全局ID进行清除,在Update里面添加下面的代码
if (recTList.Count > 0)
RecycleTid();
private void RecycleTid()
{
for(int i = 0; i < recTList.Count; i++)
{
int tid = recTList[i];
for(int j = 0; j < tidList.Count; j++)
{
if(tidList[j]== tid)
{
tidList.Remove(j);
break;
}
}
}
recTList.Clear();
}
帧定时任务开发
参考之前的TimeTask来开发FrameTask
首先是PETimeTask类中
//支持帧定时的类
class PEFrameTask
{
public int tid;
public int destFrame;//帧数
public int delay;
public Action callback;
public int count;//执行次数 0:表示该方法一直执行
public PEFrameTask(int tid, Action callback, int destFrame, int delay, int count)
{
this.tid = tid;
this.callback = callback;
this.destFrame = destFrame;
this.delay = delay;
this.count = count;
}
}
在TimerSystem中添加新的方法
private List<PEFrameTask> tempFrameList = new List<PEFrameTask>();//缓存列表
private List<PEFrameTask> taskFrameList = new List<PEFrameTask>();
private int frameCounter;
private void CheckFrameTask()
{
//加入缓存区中的定时任务
for (int index = 0; index < tempFrameList.Count; index++)
{
taskFrameList.Add(tempFrameList[index]);
}
tempFrameList.Clear();
frameCounter += 1;
//遍历检测任务是否达到条件
for (int index = 0; index < taskFrameList.Count; index++)
{
PEFrameTask task = taskFrameList[index];
//当实际时间小于应该运行的时间,说明程序已经运行过,跳过
if (frameCounter < task.destFrame)
{
continue;
}
else
{
Action cb = task.callback;
try
{
if (cb != null)
{
cb();
}
}
catch (Exception e)
{
Debug.Log(e.ToString());
}
//移除已经完成的任务
if (task.count == 1)
{
taskFrameList.RemoveAt(index);
index--;
recTList.Add(task.tid);
}
else
{
if (task.count != 0)
task.count -= 1;
task.destFrame += task.delay;
}
}
}
}
#region FrameTask
public int AddFrameTask(Action callback, int delay, int count = 1)
{
int tid = GetTid();
PEFrameTask frameTask = new PEFrameTask(tid, callback, frameCounter+delay, delay, count);
tempFrameList.Add(frameTask);
tidList.Add(tid);
return tid;
}
public bool DeleteFrameTask(int tid)
{
bool exist = false;
for (int i = 0; i < taskFrameList.Count; i++)
{
PEFrameTask task = taskFrameList[i];
if (task.tid == tid)
{
taskFrameList.RemoveAt(i);
for (int j = 0; j < tidList.Count; j++)
{
if (tidList[j] == tid)
{
tidList.RemoveAt(j);
break;
}
}
}
exist = true;
break;
}
if (!exist)
{
for (int i = 0; i < tempFrameList.Count; i++)
{
PEFrameTask task = tempFrameList[i];
if (task.tid == tid)
{
taskFrameList.RemoveAt(i);
for (int j = 0; j < tidList.Count; j++)
{
if (tidList[j] == tid)
{
tidList.RemoveAt(j);
break;
}
}
}
exist = true;
break;
}
}
return exist;
}
public bool ReplaceFrameTask(int tid, Action callback, int delay, int count = 1)
{
PEFrameTask newTask = new PEFrameTask(tid, callback, frameCounter+delay, delay, count);
bool isRep = false;
for (int i = 0; i < taskFrameList.Count; i++)
{
if (taskFrameList[i].tid == tid)
{
taskFrameList[i] = newTask;
isRep = true;
break;
}
}
if (!isRep)
{
for (int i = 0; i < tempFrameList.Count; i++)
{
if (tempFrameList[i].tid == tid)
{
tempFrameList[i] = newTask;
isRep = true;
break;
}
}
}
return isRep;
}
#endregion
测试帧定时任务
在GameRoot里面添加三个按钮进行检测
public void ClickAddFrameBtn()
{
//使用Lamda表达式添加任务
Debug.Log("Add Frame Task");
tid = TimerSystem.Instance.AddFrameTask(() =>
{
Debug.Log("Frame tid:" + tid + " " + System.DateTime.Now);
}, 50, 0);
}
public void ClickDelFrameBtn()
{
Debug.Log("Del Time Task");
bool ret = TimerSystem.Instance.DeleteFrameTask(tid);
Debug.Log("Del Frame Task:" + ret);
}
public void ClickRepFrameBtn()
{
Debug.Log("Rep Frame Task");
bool ret = TimerSystem.Instance.ReplaceFrameTask(tid, FuncB, 100);
Debug.Log("Rep Frame Task:" + ret);
}