最近的诺兰的电影信条的上映让我们对时间的概念又发生了一次转变,如果电影中这种机制出现在游戏中是如何实现的呢?下面是实现效果。
在游戏中我们可以分出两个角色,分为正时间和逆时间的角色,当玩家主动触发技能后,从正时间的角色中分离出逆时间的角色,玩家操控逆时间的角色,这时世界进入逆时间,而正时间的角色就自动时间倒流(在逆时间角色眼中);当玩家再次触发技能,则世界回到正时间,玩家可操纵角色也转移到正时间角色上,此时逆时间的角色就开始时间倒流(在正时间角色眼里)。要实现这个效果我们可以利用循环队列的方式,通过动态加入角色的状态来记录物体之前的位置信息,等到需要倒流时,再通过循环队列逆向播放就可以实现这个效果,而对于逆时间的角色,记录信息时在队列中是反向记录的,播放时是正向的,这样就能实现正时间的和逆时间的都能看到对方是逆向的。此外,对于逆时间的物体来说,遵循受力方向不变,但速度(角速度)反向的定律。
这是我们大体思路,下面是代码实现。
public class TraceableObject // 记录可回溯物体的历史参数 { public GameObject ob; public Vector3[] pos = new Vector3[Length]; // [j]:物体在第j个时间片的位置,循环队列(用Vector3.x == 9999 表示还未生成时) public Quaternion[] rot = new Quaternion[Length]; public Vector2[] velocity = new Vector2[Length]; // [j]:物体在第j个时间片的速度,循环队列 public float[] angularVelocity = new float[Length]; public RigidbodyType2D[] bodyType = new RigidbodyType2D[Length]; // [j]:物体的刚体类型(可能会用脚本变换) public Sprite[] sprite = new Sprite[Length]; public int[] flip = new int[Length]; public bool isTouchedCurrentPlayer = false; // 【TENET】是否因碰到当前控制的龟而“不恢复只记录”(每次时间变向时恢复初值) }
每一个时间片记录物体的每一个时间对应的位置pos,角速度angularVelocity,旋转rot,速度velocity,刚体类型bodyType, 以及逆向播放的动画图片sprite,某些物体(龟和乌贼)还有朝向的状态,用flip来记录,bool类型isTouchedCurrentPlayer来判断是否实现当在逆时间里,没有被角色碰撞的物体会虚化的特效。这些定义的变量封装成一个类,就可以直接挂在可回溯的物体上。
int tail = 0; // 指向下一个要进队的位置 int total = 0; // 已经经过了多少个正向时间片(用于控制倒流时长,和石钟的转动角度) ChangeColorToBlackAndWhite BlackAndWhite; // 【TENET】 bool isInverse = false; // 是否是逆时间。点击倒流,变成true,再点倒流,变成false GameObject player; // 原龟(正时间) GameObject player1 = null; // 蓝龟(逆时间) int inverseTotal = 0; // 已经经过了多少个逆向时间片(用于控制正向倒流时长,和石钟的转动角度)
定义的isInverse来判断是否是在逆时间,这是一个是否逆向的重要判断依据。
定义完变量后就来先来实现逻辑代码,在start函数中,主要是创建BackInTime链表,调用addToBackInTime函数依次动态加入可回溯对象,在addToBackInTime函数中又调用Record函数让每个对象又记录当前状态到时间片i上,并开启RecordHistory携程。
void Start() //void Awake() { BlackAndWhite = GameObject.Find("B&W").GetComponent<ChangeColorToBlackAndWhite>(); clock =Global.UI.transform.Find("StoneClock").gameObject.GetComponent<StoneClock>(); BackInTime = new LinkedList<TraceableObject>(); if(backInTime != null) foreach (var ob in backInTime) addToBackInTime(ob, false); //* // 记录绳子的每个片段消失的时间,倒流后比较以决定是否恢复 if (Rattans != null) // 提取绳子的片段 { foreach (GameObject rattan in Rattans) { Transform[] ps = rattan.GetComponentsInChildren<Transform>(); // 含孙子对象 for (int i = 1; i < ps.Length; i++) addToBackInTime(ps[i].gameObject, false); } } //*/ //if (backInTime != null) //{ // foreach (TraceableObject tb in backInTime) // { // if (tb.ob.GetComponent<ITraceable>() != null) // tb.ob.GetComponent<ITraceable>().Init(Length); // } //} player = Global.Player; StartCoroutine("RecordHistory"); }
addToBackInTime函数实现动态加入可回溯物体,定义一个TraceableObject类型的对象,将队列中的GameObject类型的actor加入到tb中,以当下时刻状态填充进所有时间的状态。
然后加入到已定义的BackInTime链表中,如果该对象带着有其他状态,就保存。
public void addToBackInTime(GameObject actor, bool bDeleteUnborn = true) { //backInTime.Add(actor); TraceableObject tb = new TraceableObject(); tb.ob = actor; if (bDeleteUnborn) { for (int i = 0; i < tb.pos.Length; i++) { tb.pos[i] = new Vector3(9999, 0, 0); // 用Vector3.x == 9999 表示还未生成时的值 } } else { for (int i = 0; i < tb.pos.Length; i++) { // 以当下时刻状态填充进所有时间的状态 Record(tb, i); } } BackInTime.AddLast(tb); if (actor.GetComponent<ITraceable>() != null) actor.GetComponent<ITraceable>().Init(Length); }
Record函数是记录可回溯对象tb当前状态到时间片i上,包括位置、旋转、角色的滑动、以及当前状态的贴图,对于带着刚体的物体,还要记录其刚体类型、速度和角速度 。
void Record(TraceableObject tb, int i) { tb.pos[i] = tb.ob.transform.position; tb.rot[i] = tb.ob.transform.rotation; tb.flip[i] = (int)Mathf.Sign(tb.ob.transform.localScale.x); tb.sprite[i] = tb.ob.GetComponent<SpriteRenderer>().sprite; if (tb.ob.GetComponent<Rigidbody2D>()) // 若带刚体 { tb.bodyType[i] = tb.ob.GetComponent<Rigidbody2D>().bodyType; tb.velocity[i] = tb.ob.GetComponent<Rigidbody2D>().velocity; tb.angularVelocity[i] = tb.ob.GetComponent<Rigidbody2D>().angularVelocity; } if (tb.ob.GetComponent<ITraceable>() != null) // 若带附加状态 { tb.ob.GetComponent<ITraceable>().Save(i); } }
RecordHistory携程就是在没有进入回溯时的正向记录。将BackInTime链表中的可回溯物体一一通过Record函数记录历史物理参数,Record函数传入的两个参数是TraceableObject类型的对象tb和时间片i,时间片就是当前循环队列的位置,而循环队列就是普通队列,只是在记录位置时,每次自加的时候余上最大长度,即
tail = (tail + 1) % Length; total++;
每一次返回时就延迟一段历史时间间隔(我们自己定义的SamplingInterval=0.01f)
IEnumerator RecordHistory() { //GameObject circleProcess = GameObject.Find("UI Root/UI/StoneClock/CircleProcessBar"); GameObject circleProcess = Global.UI.transform.Find("StoneClock/CircleProcessBar").gameObject; while (true) { foreach (var tb in BackInTime) // 记录历史物理参数 { Record(tb, tail); } tail = (tail + 1) % Length; total++; if (1f - circleProcess.GetComponent<CircleProcess>().process > 0.01f) // 转动石钟 { circleProcess.GetComponent<CircleProcess>().process += 1f / Length; } else circleProcess.SetActive(false); yield return new WaitForSeconds(SamplingInterval); } }
完成了上述功能,剩下的就是在技能触发时触发的事件函数(单击鼠标右键)
在另一个脚本PlayerEvents中的TouchEvent()中则是该事件的触发
else if (sm.GetCurMystery().GetComponent<MYBackInTimeDLC>()) { if (!sm.GetCurMystery().GetComponent<MYBackInTimeDLC>().CantBack) { sm.GetCurMystery().GetComponent<MYBackInTimeDLC>().OnBackInTime(); return true; } else return false; }
定义的bool类型的CantBack全局变量用来判断倒流结束后一定时间内不能再次触发倒流。
public bool CantBack = false; //倒流结束后一定时间内不能再次触发倒流(防止频繁的倒流赖关)
在触发事件后,由于CanBack初始值为false,就调用了OnBackInTime函数,在一开始就判断CantBack的值来防止多次点击多次进入这个函数。
if (CantBack) return;
首先先关闭RecordHistory携程,不再正向记录物体状态,并将记录在BackInTime链表中的可回溯物体一一关闭其关闭物体的受力,速度和角速度清零。
StopCoroutine("RecordHistory"); foreach (var tb in BackInTime) // 关闭物体的受力,速度和角速度清零 { if (tb.ob.GetComponent<Rigidbody2D>()) { tb.ob.GetComponent<Rigidbody2D>().velocity = Vector2.zero; tb.ob.GetComponent<Rigidbody2D>().angularVelocity = 0; } }
然后通过前面定义的isInverse来判断当前是否是逆时间,来生成蓝龟(反方向角色),如果之前生成了蓝龟就先摧毁掉,以免出现多只蓝龟。并令当前为逆时间。
GameObject PlayerClone = GameObject.Find("Player(Clone)"); if (PlayerClone) DestroyObject(PlayerClone); isInverse = true;
在蓝龟实例化之前先关闭Player之间的碰撞,以免一开始就由于碰撞而游戏失败。
Physics2D.IgnoreLayerCollision(LayerMask.NameToLayer("Player"), LayerMask.NameToLayer("Player"), true);
然后实例化蓝龟,位置、重量为红龟(正方向角色)的位置、重量,并改为蓝色。实例化后将其通过addToBackInTime函数将其加入到BackInTime可回溯物体的链表中。
player1 = (GameObject)GameObject.Instantiate(Resources.Load("DLC TENET\Player")); player1.transform.position = player.transform.position; player1.GetComponent<Rigidbody2D>().mass = player.GetComponent<Rigidbody2D>().mass; var renderer = player1.GetComponentInChildren<Renderer>(); // 改颜色 renderer.material.color = new Color(0, 1, 1); Global.Player = player1; addToBackInTime(player1);
定义的全局变量inverseTotal是记录逆向时间片,在开始倒流后将其初始化为0,同时将正方向的龟的动画关闭,以便展示回溯时的贴图。这时是逆时间,所以音乐倒放,并加蓝色环境光。然后开启BackToHistory携程。
inverseTotal = 0; player.GetComponent<Animator>().enabled = false; // 原龟的动画关闭,以便展示回溯时的贴图 // 加蓝色环境光效果 var fade = Global.UI.GetComponent<UIManager>().Fade; fade.GetComponent<Image>().DOPause(); fade.GetComponent<Image>().color = new Color(0, 0, 1, 0); fade.GetComponent<Image>().DOFade(0.2f, 2) // 背景音乐2倍速倒放(Pitch为-2) Global.SceneEvent.GetComponents<AudioSource>()[0].pitch = -2; StartCoroutine("BackToHistory");
下面是BackToHistory携程。作用是记录逆时间里逆物体的状态和回放正时间物体的记录。
正向回溯时间固定为Length长度(最大为20s)。首先判断当前如果是正向并正时间大于0(正时间未倒流完)或反向并逆时间大于0(逆时间未倒流完),就要同时记录正或逆向的物体状态。
当isInverse=true,即当前是逆时间,开始倒流正向物体。即在队列的位置tail自减,正向时间自减,逆向时间自加。
total--; tail = (tail - 1 + Length) % Length; // 上一个位置 inverseTotal++;
如果正向的时间total=0时(逆时间结束),就恢复历史状态(这里只有动画、环境光和音乐),逆时间的记录就是在循环队列中的位置为上一个位置。
tail = (tail - 1 + Length) % Length;
if (total <= 0) // 逆时间结束 { isInverse = false; // 逆时间结束后自动翻转成正时间 Global.Player = player; tail = (tail - 1 + Length) % Length; // 上一个位置(这样正好) CleanTouchedFlag(); player.GetComponent<Animator>().enabled = true; // 玩家自由控制原龟,它的动画开启 player1.GetComponent<Animator>().enabled = false; // 蓝龟的动画关闭,以便展示回溯时的贴图 // 去蓝色环境光效果 var fade = Global.UI.GetComponent<UIManager>().Fade; fade.GetComponent<Image>().DOFade(0, 2)/*.SetEase(Ease.Linear)*/; // 背景音乐倒放还原 Global.SceneEvent.GetComponents<AudioSource>()[0].pitch = 1; continue; }
而在正时间时和逆时间相反,在循环队列中的记录为下一个位置。
inverseTotal--; tail = (tail + 1) % Length; // 下一个位置 total++;
如果正时间结束了就继续保持正时间并将蓝龟从可回溯物体中删除并摧毁。
if (inverseTotal <= 0) // 正时间结束 { removeFromBackInTime(player1); Destroy(player1); break; // 正时间结束后继续保持正时间 }
当完成逆时间、正时间的时间片记录(自加自减)后就开始恢复历史时刻的位置和角度。
foreach将BackInTime链表中的可回溯物体遍历,在逆时间不用恢复蓝龟,在正时间不用恢复原龟,是“不恢复只记录”的物体(用isTouchedCurrentPlayer记录的,如果玩家碰撞了就改变的变量),就将通过ChangeAlpha函数将其变实化。
//{{ 不用恢复历史的: if ((isInverse && tb.ob == player1) || // 逆时间不用恢复蓝龟; (!isInverse && tb.ob == player) || // 正时间不用恢复原龟 (tb.isTouchedCurrentPlayer))// 如果是“不恢复只记录”的物体 { // 不回溯的物体都变实【待优化?】 ChangeAlpha(tb.ob, 1f); continue; }
定义一个和BackInTime一样类型的链表willRemove,如果是不需要恢复历史的物体,就将其加入到这个链表,方便后面删除摧毁。如果需要恢复,就调用Recovery函数恢复,并半虚化。
if (tb.pos[tail].x == 9999) // 用Vector3.x == 9999 表示此时还未生成,需删除该对象 { willRemove.Add(tb); } else { Recovery(tb); // 回溯的物体都变半虚【待优化?】 ChangeAlpha(tb.ob, 0.5f); } foreach (var tb in willRemove) { Destroy(tb.ob); BackInTime.Remove(tb); }
如果是含物理的,就用下面的代码,并每0.02s返回一次。
/// 正、逆时间里记录必要的状态(含物理) //{{ foreach (var tb in BackInTime) // 记录历史物理参数 if ((isInverse && tb.ob == player1) || // 逆时间里记录蓝龟 (!isInverse && tb.ob == player) || // 正时间里记录原龟 tb.ob == tb.isTouchedCurrentPlayer) // 只有变实才需要另外记录,不然,原记录不用改 Record(tb, tail); //}} yield return new /*WaitForSeconds*/WaitForSecondsRealtime(SamplingInterval); // 0.02s播放一帧(提高了5倍)
在要回溯完时,调用RecoveryPhysics函数将BackInTime里的物体变实。并又开启RecordHistory携程,又开始在正时间中正向记录状态。
foreach (var tb in BackInTime) // 回溯的尽头,恢复物理属性,以及当时的速度和角速度;断裂绳子恢复 { RecoveryPhysics(tb); // 物体都变实 ChangeAlpha(tb.ob, 1f); } StartCoroutine("RecordHistory");
上面代码调用了Recovery函数,该函数就是用在队列中的位置来恢复可回溯物体的位置、角度、大小、龟的滑动和贴图。
void Recovery(TraceableObject tb) { tb.ob.transform.position = tb.pos[tail]; tb.ob.transform.rotation = tb.rot[tail]; Vector3 scale = tb.ob.transform.localScale; tb.ob.transform.localScale = new Vector3(tb.flip[tail] * Mathf.Abs(scale.x), scale.y, scale.z); tb.ob.GetComponent<SpriteRenderer>().sprite = tb.sprite[tail]; }
物体被当前龟直接或间接(喷水等)触碰,则变实。
public void ObjectTouchCurrentPlayer(GameObject ob) { foreach (var tb in BackInTime) { if (tb.ob == ob && !tb.isTouchedCurrentPlayer) { tb.isTouchedCurrentPlayer = true; RecoveryPhysics(tb); break; } } }
下面是完整代码。(其中不用管时钟和绳子类的代码)
using System.Collections; using System.Collections.Generic; using UnityEngine; using DG.Tweening; using UnityEngine.UI; using UnityEngine.SceneManagement; /// <summary> /// 给谜题中某些对象添加可回溯功能 /// 外部方法: /// void addToBackInTime(GameObject actor):在屏幕外侧一段距离处生成物体时即调用 /// void removeFromBackInTime(GameObject actor):在屏幕外侧一段距离处销毁物体时调用 /// </summary> /// /// /// <summary> /// 【TENET】操作 /// 倒流时常加长,记录间隔缩短;常速倒流;取消黑白特效 /// 点倒流,生成蓝龟(逆时间),视角与控制都转移到蓝龟身上,倒流时从循环队列里腾出的数据逆向记录逆时间的状态 /// 再点倒流,视角与控制都转移到原龟身上,蓝龟倒流 /// 红蓝龟第一次分离后,如果再接触则游戏失败(与影片一致,也避免了赖皮过关) /// 其他可回溯物体在回溯时如果与正在控制的龟接触,则立即退出回溯列表 /// /// 逆时间时物体遵循“受力方向不变,速度(角速度)反向”的定律 /// </summary> public class MYBackInTimeDLC : MonoBehaviour { /// <summary> /// 回溯的真实时间是(最长)MaxTimeInBack 秒 /// </summary> const float MaxTimeInBack = 20; // 回溯最长时间(秒) const float SamplingInterval = 0.01f; // 记录历史时间间隔(秒) const int Length = (int)(MaxTimeInBack / SamplingInterval); // 记录历史的最大帧数 //const int MaxObject = 200; // 最多需要回溯的物体个数 StoneClock clock; public class TraceableObject // 记录可回溯物体的历史参数 { public GameObject ob; public Vector3[] pos = new Vector3[Length]; // [j]:物体在第j个时间片的位置,循环队列(用Vector3.x == 9999 表示还未生成时) public Quaternion[] rot = new Quaternion[Length]; public Vector2[] velocity = new Vector2[Length]; // [j]:物体在第j个时间片的速度,循环队列 public float[] angularVelocity = new float[Length]; public RigidbodyType2D[] bodyType = new RigidbodyType2D[Length]; // [j]:物体的刚体类型(可能会用脚本变换) public Sprite[] sprite = new Sprite[Length]; public int[] flip = new int[Length]; public bool isTouchedCurrentPlayer = false; // 【TENET】是否因碰到当前控制的龟而“不恢复只记录”(每次时间变向时恢复初值) } //#define Length (int)(TimeInBack / 0.1) [SerializeField] [Tooltip("可移动物体")] List<GameObject> backInTime; //[SerializeField] //GameObject Dynamic; [HideInInspector] public LinkedList<TraceableObject> BackInTime;// = new LinkedList<TraceableObject>(); [SerializeField] [Tooltip("绳子(可消失物体)")] GameObject[] Rattans; //[SerializeField] //[Tooltip("若回溯到生成前是否删除此物体")] //bool isDeleteUnborn = true; int tail = 0; // 指向下一个要进队的位置 int total = 0; // 已经经过了多少个正向时间片(用于控制倒流时长,和石钟的转动角度) ChangeColorToBlackAndWhite BlackAndWhite; // 【TENET】 bool isInverse = false; // 是否是逆时间。点击倒流,变成true,再点倒流,变成false GameObject player; // 原龟(正时间) GameObject player1 = null; // 蓝龟(逆时间) int inverseTotal = 0; // 已经经过了多少个逆向时间片(用于控制正向倒流时长,和石钟的转动角度) // Use this for initialization void Start() //void Awake() { BlackAndWhite = GameObject.Find("B&W").GetComponent<ChangeColorToBlackAndWhite>(); clock = Global.UI.transform.Find("StoneClock").gameObject.GetComponent<StoneClock>(); BackInTime = new LinkedList<TraceableObject>(); if (backInTime != null) foreach (var ob in backInTime) addToBackInTime(ob, false); //* // 记录绳子的每个片段消失的时间,倒流后比较以决定是否恢复 if (Rattans != null) // 提取绳子的片段 { foreach (GameObject rattan in Rattans) { Transform[] ps = rattan.GetComponentsInChildren<Transform>(); // 含孙子对象 for (int i = 1; i < ps.Length; i++) addToBackInTime(ps[i].gameObject, false); } } //*/ //if (backInTime != null) //{ // foreach (TraceableObject tb in backInTime) // { // if (tb.ob.GetComponent<ITraceable>() != null) // tb.ob.GetComponent<ITraceable>().Init(Length); // } //} player = Global.Player; StartCoroutine("RecordHistory"); } /// <summary> /// 动态加入可回溯对象(bDeleteUnborn = false 可防止Player被删,以及防止玩家“SL大法” /// </summary> /// <param name="actor"></param> /// <param name="bDeleteUnborn">若回溯结束时add还未执行,则删除本物体</param> public void addToBackInTime(GameObject actor, bool bDeleteUnborn = true) { //backInTime.Add(actor); TraceableObject tb = new TraceableObject(); tb.ob = actor; if (bDeleteUnborn) { for (int i = 0; i < tb.pos.Length; i++) { tb.pos[i] = new Vector3(9999, 0, 0); // 用Vector3.x == 9999 表示还未生成时的值 } } else { for (int i = 0; i < tb.pos.Length; i++) { // 以当下时刻状态填充进所有时间的状态 Record(tb, i); } } BackInTime.AddLast(tb); if (actor.GetComponent<ITraceable>() != null) actor.GetComponent<ITraceable>().Init(Length); } //public void removeCoralFromBackInTimeWithName(string name) //{ // foreach (var tb in BackInTime) // { // if (tb.ob.name.Length >= 5) // { // if (tb.ob.name.Substring(0, 5) == name) // { // BackInTime.Remove(tb); // //tb.ob.GetComponent<光圈上的符文>().active = true; // } // } // } //} public void removeFromBackInTime(GameObject actor) { foreach (var tb in BackInTime) if (tb.ob == actor) { BackInTime.Remove(tb); break; } } /// <summary> /// 非正、逆回溯下的普通正向记录 /// </summary> /// <returns></returns> IEnumerator RecordHistory() { //GameObject circleProcess = GameObject.Find("UI Root/UI/StoneClock/CircleProcessBar"); GameObject circleProcess = Global.UI.transform.Find("StoneClock/CircleProcessBar").gameObject; while (true) { foreach (var tb in BackInTime) // 记录历史物理参数 { Record(tb, tail); } tail = (tail + 1) % Length; total++; if (1f - circleProcess.GetComponent<CircleProcess>().process > 0.01f) // 转动石钟 { circleProcess.GetComponent<CircleProcess>().process += 1f / Length; } else circleProcess.SetActive(false); yield return new WaitForSeconds(SamplingInterval); } } //bool isBackingInTime = false; // 防止嵌套运行回溯【TENET 不必使用这个】 bool bBigBackInTime = false; // 防止反复关底大回溯 public bool CantBack = false; //倒流结束后一定时间内不能再次触发倒流(防止频繁的倒流赖关) void ControlBack() { clock.GetComponent<StoneClock>().BackColor(); CantBack = false; } public void OnBackInTime() { if (CantBack) return; clock.GetComponent<StoneClock>().ChangeColor(); CantBack = true; Invoke("ControlBack", 1f); // 关底大回溯 if (SceneManager.GetActiveScene().name == "Game3" && Global.SceneEvent.GetCurrentMyIndex() == Global.SceneEvent.GetMysteryCount() - 1) // 关底大回溯 { if (bBigBackInTime == false) { // 黑白特效 BlackAndWhite.Change(); bBigBackInTime = true; BigBackInTime(); } return; } //if (!isBackingInTime) { //isBackingInTime = true; // 黑白特效 // BlackAndWhite.Change(); StopCoroutine("RecordHistory"); foreach (var tb in BackInTime) // 关闭物体的受力,速度和角速度清零 { if (tb.ob.GetComponent<Rigidbody2D>()) { //tb.ob.GetComponent<Rigidbody2D>().isKinematic = true; // isKinematic设置成true再改回false,刚体上的关节可能会失效! //if(ob.GetComponent<Collider2D>()) // ob.GetComponent<Collider2D>().enabled = false; tb.ob.GetComponent<Rigidbody2D>().velocity = Vector2.zero; tb.ob.GetComponent<Rigidbody2D>().angularVelocity = 0; } // 半透明 //Color c = ob.GetComponent<SpriteRenderer>().color; //c.a = 0.5f; //ob.GetComponent<SpriteRenderer>().color = c; } // 【TENET】 if (!isInverse) // 生成蓝龟,并加入倒流列表 { //摧毁掉之前生成的蓝龟 GameObject PlayerClone = GameObject.Find("Player(Clone)"); if (PlayerClone) DestroyObject(PlayerClone); isInverse = true; //【TENET】关闭Player之间的碰撞 Physics2D.IgnoreLayerCollision(LayerMask.NameToLayer("Player"), LayerMask.NameToLayer("Player"), true); player1 = (GameObject)GameObject.Instantiate(Resources.Load("DLC TENET\Player")); player1.transform.position = player.transform.position; player1.GetComponent<Rigidbody2D>().mass = player.GetComponent<Rigidbody2D>().mass; var renderer = player1.GetComponentInChildren<Renderer>(); // 改颜色 renderer.material.color = new Color(0, 1, 1); Global.Player = player1; addToBackInTime(player1); inverseTotal = 0; player.GetComponent<Animator>().enabled = false; // 原龟的动画关闭,以便展示回溯时的贴图 // 加蓝色环境光效果 var fade = Global.UI.GetComponent<UIManager>().Fade; //fade.SetActive(true); fade.GetComponent<Image>().DOPause(); fade.GetComponent<Image>().color = new Color(0, 0, 1, 0); fade.GetComponent<Image>().DOFade(0.2f, 2)/*.SetEase(Ease.Linear)*/; // 背景音乐2倍速倒放(Pitch为-2) Global.SceneEvent.GetComponents<AudioSource>()[0].pitch = -2; StartCoroutine("BackToHistory"); } else // 回到原龟(有两个触发处,另一个在BackToHistory()里)。此段落也许能简化? { isInverse = false; Global.Player = player; tail = (tail - 1 + Length) % Length; // 上一个位置(这样正好) total = 0; player.GetComponent<Animator>().enabled = true; // 玩家自由控制原龟,它的动画开启 player1.GetComponent<Animator>().enabled = false; // 蓝龟的动画关闭,以便展示回溯时的贴图 // 去蓝色环境光效果 var fade = Global.UI.GetComponent<UIManager>().Fade; fade.GetComponent<Image>().DOFade(0, 2)/*.SetEase(Ease.Linear)*/; // 背景音乐倒放还原 Global.SceneEvent.GetComponents<AudioSource>()[0].pitch = 1; } CleanTouchedFlag(); } } IEnumerator BackToHistory() { // 确定回溯的总长 // if (total > Length) // 去掉本行可以让回溯时长达到最大 total = Length; // 石钟倒流 clock.OnBackInTime(total, Length); // 如果处于逆时间并且正时间没倒流完,或者处于正时间并且逆时间没倒流完,则倒流,同时记录(另一个时间方向的历史) // 不然,只记录(另一个时间方向的历史) if ((isInverse && total > 0) || (!isInverse && inverseTotal > 0)) { // 正或逆回溯,同时记录 while (true) { //// ///正、逆时间的长度控制和转换 /// if (isInverse) // 逆时间状态:负向恢复历史状态 { if (total <= 0) // 逆时间结束 { isInverse = false; // 逆时间结束后自动翻转成正时间 Global.Player = player; tail = (tail - 1 + Length) % Length; // 上一个位置(这样正好) CleanTouchedFlag(); player.GetComponent<Animator>().enabled = true; // 玩家自由控制原龟,它的动画开启 player1.GetComponent<Animator>().enabled = false; // 蓝龟的动画关闭,以便展示回溯时的贴图 // 去蓝色环境光效果 var fade = Global.UI.GetComponent<UIManager>().Fade; fade.GetComponent<Image>().DOFade(0, 2)/*.SetEase(Ease.Linear)*/; // 背景音乐倒放还原 Global.SceneEvent.GetComponents<AudioSource>()[0].pitch = 1; continue; } total--; tail = (tail - 1 + Length) % Length; // 上一个位置 inverseTotal++; } else // 顺时间状态:正向恢复“(逆时间的)历史”状态 { if (inverseTotal <= 0) // 正时间结束 { removeFromBackInTime(player1); Destroy(player1); break; // 正时间结束后继续保持正时间 } inverseTotal--; tail = (tail + 1) % Length; // 下一个位置 total++; } //// /// 恢复历史时刻的状态(不含物理) /// List<TraceableObject> willRemove = new List<TraceableObject>(); // 【将来改为直接在链表上删除,提高效率】 foreach (var tb in BackInTime) // 恢复历史时刻的位置和角度 { //{{ 不用恢复历史的: if ((isInverse && tb.ob == player1) || // 逆时间不用恢复蓝龟; (!isInverse && tb.ob == player) || // 正时间不用恢复原龟 (tb.isTouchedCurrentPlayer))// 如果是“不恢复只记录”的物体 { // 不回溯的物体都变实【待优化?】 ChangeAlpha(tb.ob, 1f); continue; } //}} //{{ 需要恢复历史的: if (tb.pos[tail].x == 9999) // 用Vector3.x == 9999 表示此时还未生成,需删除该对象 { willRemove.Add(tb); } else { Recovery(tb); // 回溯的物体都变半虚【待优化?】 ChangeAlpha(tb.ob, 0.5f); } //}} } foreach (var tb in willRemove) { Destroy(tb.ob); BackInTime.Remove(tb); } //// /// 正、逆时间里记录必要的状态(含物理) //{{ foreach (var tb in BackInTime) // 记录历史物理参数 if ((isInverse && tb.ob == player1) || // 逆时间里记录蓝龟 (!isInverse && tb.ob == player) || // 正时间里记录原龟 tb.ob == tb.isTouchedCurrentPlayer) // 只有变实才需要另外记录,不然,原记录不用改 Record(tb, tail); //}} yield return new /*WaitForSeconds*/WaitForSecondsRealtime(SamplingInterval); // 0.02s播放一帧(提高了5倍) } foreach (var tb in BackInTime) // 回溯的尽头,恢复物理属性,以及当时的速度和角速度;断裂绳子恢复 { RecoveryPhysics(tb); // 物体都变实 ChangeAlpha(tb.ob, 1f); } } // 关闭黑白特效 // BlackAndWhite.Change(); //Global.Player.GetComponent<PlayerEventsNetwork>().RpcBackTime(); StartCoroutine("RecordHistory"); //isBackingInTime = false; } /// <summary> /// 记录tb当前状态到时间片i上 /// </summary> /// <param name="tb"></param> /// <param name="i"></param> void Record(TraceableObject tb, int i) { if (tb.ob == null) return; tb.pos[i] = tb.ob.transform.position; tb.rot[i] = tb.ob.transform.rotation; tb.flip[i] = (int)Mathf.Sign(tb.ob.transform.localScale.x); tb.sprite[i] = tb.ob.GetComponent<SpriteRenderer>().sprite; if (tb.ob.GetComponent<Rigidbody2D>()) // 若带刚体 { tb.bodyType[i] = tb.ob.GetComponent<Rigidbody2D>().bodyType; tb.velocity[i] = tb.ob.GetComponent<Rigidbody2D>().velocity; tb.angularVelocity[i] = tb.ob.GetComponent<Rigidbody2D>().angularVelocity; } if (tb.ob.GetComponent<ITraceable>() != null) // 若带附加状态 { tb.ob.GetComponent<ITraceable>().Save(i); } } /// <summary> /// 恢复历史时刻的状态(不含物理) /// </summary> /// <param name="tb"></param> void Recovery(TraceableObject tb) { if (tb.ob == null) return; tb.ob.transform.position = tb.pos[tail]; tb.ob.transform.rotation = tb.rot[tail]; Vector3 scale = tb.ob.transform.localScale; tb.ob.transform.localScale = new Vector3(tb.flip[tail] * Mathf.Abs(scale.x), scale.y, scale.z); tb.ob.GetComponent<SpriteRenderer>().sprite = tb.sprite[tail]; } /// <summary> /// 当tb转为正常物体时,恢复tb的物理属性,和独有状态 /// </summary> /// <param name="tb"></param> void RecoveryPhysics(TraceableObject tb) { if (tb.ob.GetComponent<Rigidbody2D>()) { //tb.ob.GetComponent<Rigidbody2D>().isKinematic = false; //if(backInTime[i].GetComponent<Collider2D>()) // backInTime[i].GetComponent<Collider2D>().enabled = true; tb.ob.GetComponent<Rigidbody2D>().bodyType = tb.bodyType[tail]; if (isInverse) // 逆时间里恢复的速度(角速度)都是反向的 { tb.ob.GetComponent<Rigidbody2D>().velocity = -tb.velocity[tail]; tb.ob.GetComponent<Rigidbody2D>().angularVelocity = -tb.angularVelocity[tail]; } else { tb.ob.GetComponent<Rigidbody2D>().velocity = tb.velocity[tail]; tb.ob.GetComponent<Rigidbody2D>().angularVelocity = tb.angularVelocity[tail]; } //if(backInTime[i].GetComponent<RattanDelete>()) // 是绳子 //{ // if (backInTime[i].GetComponent<RattanDelete>().Time > Time.time - TimeInBack) // 断裂在回溯起点后 // { // backInTime[i].GetComponent<HingeJoint2D>().enabled = true; // } //} } if (tb.ob.GetComponent<ITraceable>() != null) { tb.ob.GetComponent<ITraceable>().Load(tail); } } void CleanTouchedFlag() { foreach (var tb in BackInTime) { tb.isTouchedCurrentPlayer = false; } } /// <summary> /// 物体被当前龟直接或间接(喷水等)触碰,则变实 /// </summary> /// <param name="tb"></param> public void ObjectTouchCurrentPlayer(GameObject ob) { foreach (var tb in BackInTime) { if (tb.ob == ob && !tb.isTouchedCurrentPlayer) { tb.isTouchedCurrentPlayer = true; RecoveryPhysics(tb); break; } } } /// <summary> /// 改变物体(原、蓝龟除外)的透明度(包括子物体) /// </summary> /// <param name="ob"></param> /// <param name="alpha"></param> void ChangeAlpha(GameObject ob, float alpha) { if (ob == null) return; Color color = ob.GetComponent<SpriteRenderer>().color; ob.GetComponent<SpriteRenderer>().color = new Color(color.r, color.g, color.b, alpha); if (ob.tag != "Player") { foreach (var sr in ob.GetComponentsInChildren<SpriteRenderer>()) { color = sr.color; sr.color = new Color(color.r, color.g, color.b, alpha); } } } // Update is called once per frame void Update() { } void BigBackInTime() { //Global.Player.transform.DOMoveX(Global.Player.transform.position.x * 2, 5); Camera.main.transform.parent = Global.Player.transform; Global.Player.transform.DOMoveX(/*-60*/-80, 10); StartCoroutine("reverseStoneClock"); } IEnumerator reverseStoneClock() { clock.reverse = true; yield return new /*WaitForSeconds*/WaitForSecondsRealtime(10f); clock.reverse = false; BlackAndWhite.Change(); Debug.Log("dad"); } }