Last active
July 18, 2019 17:27
-
-
Save john-holland/ec4df73d5215291d5e18 to your computer and use it in GitHub Desktop.
An implementation of a Braid-like rewind mechanic.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEngine; | |
using System.Collections; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using System.Runtime.Serialization; | |
using System; | |
using System.Collections.Generic; | |
using UnityEngine.Assertions; | |
public class TimeTravellerBehaviour : MonoBehaviour | |
{ | |
const string PRIMARY = "primary"; | |
// Use this for initialization | |
void Start() | |
{ | |
RegisterTimeStream(PRIMARY); | |
} | |
// Update is called once per frame | |
void Update() | |
{ | |
} | |
[SerializeField] | |
public string TimeStream = PRIMARY; | |
class TimeTraveller | |
{ | |
public GameObject Target { get; set; } | |
public float GameTime { get; set; } | |
public string Name { get; set; } | |
private Dictionary<string, bool> ownComponentsEnabled = new Dictionary<string, bool>(); | |
private Dictionary<int, Dictionary<string, bool>> objectChildrenComponentsEnabled = new Dictionary<int, Dictionary<string, bool>>(); | |
public bool Active { get; set; } | |
public string TimeStream { get; set; } | |
public TimeTraveller() | |
{ | |
} | |
public TimeTraveller(float gameTime, GameObject target, string timeStream) | |
{ | |
GameTime = gameTime; | |
Target = target; | |
this.TimeStream = timeStream; | |
Name = target.name; | |
//if they don't have a name, we'll make a unique one up | |
if (string.IsNullOrEmpty(Name)) | |
{ | |
Name = "TimeTraveller" + GetAndIncrementNextFreeId(); | |
Target.name = Name; | |
} | |
} | |
static long nextFreeId = 0; | |
private long GetAndIncrementNextFreeId() | |
{ | |
return nextFreeId++; | |
} | |
public void Persist() | |
{ | |
Target = GameObject.Instantiate(Target); | |
Target.name = null; | |
Deactivate(); | |
} | |
public void Create() | |
{ | |
//give them back their name... | |
Revive(); | |
} | |
private void Reactivate() | |
{ | |
Target.SetActive(Active); | |
foreach (var c in Target.GetComponents<Behaviour>()) | |
{ | |
string componentName = c.GetType().FullName; | |
if (ownComponentsEnabled.ContainsKey(componentName)) | |
{ | |
c.enabled = ownComponentsEnabled[componentName]; | |
} | |
} | |
foreach(var c in Target.GetComponentsInChildren<Behaviour>()) | |
{ | |
string componentName = c.GetType().FullName; | |
bool enabled = false; | |
if (objectChildrenComponentsEnabled.ContainsKey(c.GetInstanceID()) && | |
objectChildrenComponentsEnabled[c.GetInstanceID()].TryGetValue(componentName, out enabled)) | |
{ | |
c.enabled = enabled; | |
} | |
} | |
} | |
private void Deactivate() | |
{ | |
Active = Target.activeSelf; | |
foreach (var c in Target.GetComponents<Behaviour>()) | |
{ | |
ownComponentsEnabled[c.GetType().FullName] = c.enabled; | |
c.enabled = false; | |
} | |
//this could probably be improved by just appending fullname and instance id... | |
foreach (var c in Target.GetComponentsInChildren<Behaviour>()) | |
{ | |
Dictionary<string, bool> dict = null; | |
if (!objectChildrenComponentsEnabled.TryGetValue(c.GetInstanceID(), out dict)) { | |
dict = new Dictionary<string, bool>(); | |
} | |
dict[c.GetType().FullName] = c.enabled; | |
c.enabled = false; | |
} | |
Target.SetActive(false); | |
} | |
public void Destroy() | |
{ | |
if (Target != null && string.IsNullOrEmpty(Target.name)) | |
{ | |
Name = Target.name; | |
} | |
if (Target != null) | |
{ | |
Target.name = null; | |
GameObject.DestroyImmediate(Target); | |
Target = null; | |
Name = "Robert Paulson"; | |
} | |
} | |
public void Apply() | |
{ | |
var existingObject = GameObject.Find(Name); | |
if (existingObject != null) | |
{ | |
existingObject.name = ""; | |
GameObject.Destroy(existingObject); | |
} | |
Revive(); | |
} | |
protected GameObject Revive() | |
{ | |
Target.name = Name; | |
Reactivate(); | |
return Target; | |
} | |
} | |
static HashSet<string> livingTravellers = new HashSet<string>(); | |
private void OnDestroy() | |
{ | |
//if we're being destroyed, we should remove our name from the living travellers list. | |
if (gameObject.name != null) | |
{ | |
livingTravellers.Remove(name); | |
} | |
} | |
static bool isRewinding = false; | |
static float framesLeftToRewind = 0; | |
static Dictionary<string, List<Frame>> timeStreams = new Dictionary<string, List<Frame>>(); | |
const int FRAME_CACHE_SIZE = 300; | |
public void OnLevelWasLoaded() | |
{ | |
Debug.Log("Level loaded!"); | |
isRewinding = false; | |
framesLeftToRewind = 0; | |
Dictionary<string, List<Frame>> timeStreams = new Dictionary<string, List<Frame>>(); | |
} | |
public static void RegisterTimeStream(string timestream) | |
{ | |
if (!timeStreams.ContainsKey(timestream)) | |
{ | |
timeStreams[timestream] = new List<Frame>(); | |
} | |
} | |
static float lastFixedTime = 0; | |
public void FixedUpdate() | |
{ | |
if (lastFixedTime != Time.fixedTime) | |
{ | |
if (Input.GetKey(KeyCode.Z)) | |
{ | |
isRewinding = true; | |
} | |
else | |
{ | |
isRewinding = false; | |
} | |
if (isRewinding) | |
{ | |
rewind(TimeStream); | |
Debug.Log("rewinding" + lastFixedTime); | |
var c = GameObject.Find("GameManager"); | |
if (c != null) | |
{ | |
var cc = c.GetComponent<RewindGameManager>(); | |
if (cc != null) | |
{ | |
cc.SimulateUpdate(); | |
} | |
} | |
} | |
else | |
{ | |
var currentTimeTravellers = FindObjectsOfType<TimeTravellerBehaviour>() | |
.Select(ttb => new TimeTraveller(Time.timeSinceLevelLoad, ttb.gameObject, ttb.TimeStream)).ToList(); | |
foreach (var timeTraveller in currentTimeTravellers) | |
{ | |
push(timeTraveller.TimeStream, lastFixedTime, timeTraveller); | |
} | |
} | |
lastFixedTime = Time.fixedTime; | |
} | |
} | |
class Frame | |
{ | |
public float GameTime { get; set; } | |
public Dictionary<string, TimeTraveller> TimeTravellers { get; set; } | |
public Frame(float gameTime) | |
{ | |
GameTime = gameTime; | |
TimeTravellers = new Dictionary<string, TimeTraveller>(); | |
} | |
public void Destroy() | |
{ | |
foreach (var tt in TimeTravellers.Values) | |
{ | |
tt.Destroy(); | |
} | |
TimeTravellers.Clear(); | |
} | |
} | |
private static void push(string timeStream, float fixedTime, TimeTraveller timeTraveller) | |
{ | |
List<Frame> timeTravellers = GetTimeStream(timeStream); | |
Frame frame = null; | |
if (!timeTravellers.Any()) | |
{ | |
frame = new Frame(fixedTime); | |
timeTravellers.Add(frame); | |
} | |
frame = timeTravellers.Last(); | |
if (frame.GameTime != fixedTime) | |
{ | |
frame = new Frame(fixedTime); | |
timeTravellers.Add(frame); | |
} | |
if (frame.TimeTravellers.ContainsKey(timeTraveller.Name)) | |
{ | |
Debug.Log("Warning duplicate name! " + timeTraveller.Name); | |
} | |
else | |
{ | |
timeTraveller.Persist(); | |
frame.TimeTravellers.Add(timeTraveller.Name, timeTraveller); | |
} | |
if (timeTravellers.Count > FRAME_CACHE_SIZE && timeTravellers.Count > 1) | |
{ | |
var tt = timeTravellers.First(); | |
tt.Destroy(); | |
timeTravellers.RemoveAt(0); | |
} | |
} | |
private static void rewind(string timeStream) | |
{ | |
List<Frame> timeTravellers = GetTimeStream(timeStream); | |
if (timeTravellers.Count < 2) | |
{ | |
return; | |
} | |
var music = GameObject.Find("music"); | |
if (music != null) | |
{ | |
music.GetComponent<AudioSourceFader>().Rewind(1f); | |
} | |
//think about who we were | |
var mostRecentFrame = timeTravellers.Last(); | |
timeTravellers.RemoveAt(timeTravellers.Count - 1); | |
var currentTimeTravellers = FindObjectsOfType<TimeTravellerBehaviour>() | |
.Where(ttb => ttb.gameObject.GetComponent<TimeTravellerBehaviour>().TimeStream == timeStream) | |
.Select(ttb => new TimeTraveller(Time.timeSinceLevelLoad, ttb.gameObject, timeStream)).ToList(); | |
var currentTimeTravellerNames = currentTimeTravellers.Select(tt => tt.Name); | |
var previousTimeTravellerNames = mostRecentFrame.TimeTravellers.Keys.ToList(); | |
var survivors = currentTimeTravellerNames.Intersect(previousTimeTravellerNames); | |
var timeTravellersToDestroy = currentTimeTravellerNames.Except(survivors); | |
var timeTravellersToCreate = previousTimeTravellerNames.Except(currentTimeTravellerNames); | |
foreach (string timeTraveller in timeTravellersToDestroy) | |
{ | |
var toDestroy = currentTimeTravellers.Find(tt => tt.Name == timeTraveller); | |
if (toDestroy != null) | |
{ | |
toDestroy.Destroy(); | |
} | |
} | |
foreach (string timeTraveller in timeTravellersToCreate) | |
{ | |
var toCreate = mostRecentFrame.TimeTravellers[timeTraveller]; | |
if (toCreate != null) | |
{ | |
toCreate.Create(); | |
} | |
} | |
foreach (string timeTraveller in survivors) | |
{ | |
var toApply = mostRecentFrame.TimeTravellers[timeTraveller]; | |
if (toApply != null) | |
{ | |
toApply.Apply(); | |
} | |
} | |
} | |
private static List<Frame> GetTimeStream(string timeStream) | |
{ | |
List<Frame> timeTravellers = null; | |
if (!timeStreams.ContainsKey(timeStream)) | |
{ | |
Debug.LogWarning("Unknown timestream: " + timeStream); | |
RegisterTimeStream(timeStream); | |
} | |
timeTravellers = timeStreams[timeStream]; | |
return timeTravellers; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment