時間回溯——用Unity實現(xiàn)時空幻境(Braid)中的控制時間效果

作者:QXYO
前言
控制時間相信幾乎是每個人都想擁有的能力,也為眾多影視、游戲等提供了靈感,荒木老師在jojo的奇妙冒險中幾乎把控制時間的能力玩了個遍。而在游戲領域,令筆者印象最深的就是本次的主角——時空幻境(Braid),一款把橫板跳躍與時間回溯完美結合的游戲。

注意!由于本教程主要實現(xiàn)時間回溯效果,橫板過關類游戲的場景搭建、移動、動畫等不在本次內容范圍內,有興趣的同學可以先從B站專欄開始學起:
【簡明UNITY教程】教你迅速實現(xiàn)2D角色的移動和跳躍
本次教程也會以該系列視頻的工程為基礎,在原項目上進行修改實現(xiàn)時間回溯的功能。
基礎工程:https://pan.baidu.com/share/init?surl=HISQizt0NvCHo8U0KgSCCA
提取碼:jlbx
一、時間回溯實現(xiàn)原理
我們知道視頻是能夠倒放的,那游戲可不可以也像視頻那樣把每一幀記錄下來,需要時再倒著輸出實現(xiàn)時間倒流呢?答案當然是可以的,這種方法稱為“備忘錄模式”。事實上時空幻境的作者也說過該游戲主要是用該方法制作,有興趣、英語好的同學可以看看作者的解釋:https://news.ycombinator.com/item?id=9484197
二、用Unity實現(xiàn)時間回溯
下載好前言中提到的基礎工程,打開之后可能會有幾個不影響的警告,Clear即可。打開Scene文件夾下的SampleScene場景,運行一下,試試操作人物移動、跳躍,應該不會有什么問題。
主要實現(xiàn)操控角色的時間倒流效果,所以先把怪物(opossum-1)從場景中刪除。

1.設置保存數(shù)據類型
首先要確定每幀保存什么數(shù)據,位置數(shù)據、起跳后的速度數(shù)據,由于是2d動畫所以還需要記錄每幀所用的Sprite和臉的朝向。在Scripts文件夾里新建一個c#腳本,取名為ObjectStage。
public class ObjectStage
{
??? public Vector3 Position { get; set; }
??? public Vector3 Velocity { get; set; }
??? public Sprite Sprite { get; set; }
??? public bool IsRight { get; set; }
}
?
2.保存角色狀態(tài)
接下來就要實現(xiàn)時間倒流的效果了,新建腳本TimeBack掛到Player上。由于讀取數(shù)據是從后往前讀取,所以可以使用stack(棧)一個后進先出的容器來保存數(shù)據。同時也需要獲取到player上的<SpriteRenderer>,用于獲取和修改某一幀角色的動作;<Animator>用于在時間倒流時暫停動畫的播放;<CharacterController2D>,原工程的角色控制代碼,用于修改角色臉的朝向;<Rigidbody2D>,獲取、修改速度和時間倒流時關閉物理引擎。
??? void Start()
??? {
??????? TimeBackData = new Stack();
?????? ?SpriteRenderer = GetComponent<SpriteRenderer>();
??????? animator = GetComponent<Animator>();
??????? cc2D = GetComponent<CharacterController2D>();
??????? m_Rigidbody2D = GetComponent<Rigidbody2D>();
??? }
?
首先是保存數(shù)據,cc2D.m_FacingRight在原工程里受保護的(private)這里我們需要公開(public)。
??? void SaveData()
??? {
??????? ObjectStage stage = new ObjectStage();
??????? stage.Position = transform.position;
??????? stage.Sprite = SpriteRenderer.sprite;
??????? stage.IsRight = cc2D.m_FacingRight;
??????? stage.Velocity = m_Rigidbody2D.velocity;
??????? TimeBackData.Push(stage);
??? }
?
3.讀取和顯示狀態(tài)
接下來是讀取數(shù)據,讀取后的數(shù)據就可以刪除了,可以用Stack.Pop(),但是最后一個讀取的數(shù)據,也就是第一個保存的數(shù)據不能刪,可以用 Stack.Peek()。
??? ObjectStage LoadData()
??? {
??????? if (TimeBackData.Count > 1)
??????? {
??????????? return (ObjectStage)TimeBackData.Pop();
??????? }
??????? else
??????? {
??????????? return (ObjectStage)TimeBackData.Peek();
??????? }
??? }
?
然后就是把讀取的數(shù)據反映到Player身上,也就是時間倒流的過程,要注意在這期間角色應該是不受物理引擎的影響,并且不能播放動畫,要在代碼中關閉。
??? void ShowData(ObjectStage stage)
??? {
??????? animator.enabled = false;
??????? transform.position = stage.Position;
??????? SpriteRenderer.sprite = stage.Sprite;
??????? transform.localScale = new Vector3(stage.IsRight ? 1 : -1, 1, 1);
??????? m_Rigidbody2D.simulated = false;
??????? m_Rigidbody2D.velocity = stage.Velocity;
??? }
?
4.調用代碼實現(xiàn)時間回溯
方法寫好了,接下來就是調用了,我們知道update在一秒內執(zhí)行的次數(shù)是不固定的,所以我們保存數(shù)據和讀取數(shù)據只能放在FixedUpdate里。并且只有在按下時間倒流的按鍵時才能讀取數(shù)據,其他時間保存數(shù)據,按鍵抬起的時候要把之前關閉的物理引擎和動畫開啟。
? ObjectStage LoadStageData = new ObjectStage();?
? private void FixedUpdate()
??? {
??????? if (CheckKey)
??????? {
??????????? LoadStageData = LoadData();
??????????? if (LoadStageData != null)
??????????? {
??????????????? ShowData(LoadStageData);
??????????? }
??????? }
??????? else
??????? {
??????????? SaveData();
??????? }
??? }
?
(注意,按鍵檢測仍然要放在UpDate里。)
??? private void Update()
??? {
??????? CheckKey = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
??????? CheckKeyUp = Input.GetKeyUp(KeyCode.LeftShift) || Input.GetKeyUp(KeyCode.RightShift);
??????? if (CheckKeyUp)
??????? {
??????????? cc2D.m_FacingRight = LoadStageData.IsRight;
??????????? animator.enabled = true;
??????????? m_Rigidbody2D.simulated = true;
??????? }
??? }
?
運行游戲操作一會,再按下Shift看看你的角色是不是已經是一個無敵的存在,畢竟一個可以無限時間倒流的人是不可能會輸?shù)陌?。(某平凡的上班族點了個贊?。?/p>
如果追求細節(jié)的話能發(fā)現(xiàn),時間回溯到在空中時結束回溯,角色會垂直落下,這時候只需要把CharacterController2D腳本上的canAirControl勾選為false即可繼續(xù)跳躍。但這樣修改也有個問題,角色不能在空中移動了,為了模擬Braid原版游戲的手感,我們可以嘗試修改基礎工程的move方法。
首先把canAirControl勾選為false。
??????? if (m_Grounded || canAirControl)
??????? {
??????????? // 輸入變量move決定橫向速度
??????????? m_Rigidbody2D.velocity = new Vector2(move, m_Rigidbody2D.velocity.y);
??????? }
??????? else if (!m_Grounded)
???? ???{
??????????? if (move > 0 && m_FacingRight)
??????????? {
??????????????? m_Rigidbody2D.velocity = new Vector2(Mathf.Max(move, m_Rigidbody2D.velocity.x), m_Rigidbody2D.velocity.y);
??????????? }
??????????? else if (move < 0 && !m_FacingRight)
??????? ????{
??????????????? m_Rigidbody2D.velocity = new Vector2(Mathf.Min(move, m_Rigidbody2D.velocity.x), m_Rigidbody2D.velocity.y);
??????????? }//如果在空中有相反方向的操作則修改水平速度
??????? }
?
修改后的手感就和Braid里面非常相似了。
另外,如果想在時間回溯時音頻也跟著倒放,可以修改AudioSource組件的Pitch參數(shù)為-1。
修改后的工程:https://pan.baidu.com/share/init?surl=tmPDt9Ebq814cbasffOhlA
提取碼: m4pg

對線下游戲開發(fā)學習感興趣的盆友,歡迎訪問:http://levelpp.com/
同時,也歡迎加入游戲開發(fā)群攪基:610475807