Skip to content

Instantly share code, notes, and snippets.

@john-holland
Last active July 18, 2019 17:27
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save john-holland/ec4df73d5215291d5e18 to your computer and use it in GitHub Desktop.
Save john-holland/ec4df73d5215291d5e18 to your computer and use it in GitHub Desktop.
An implementation of a Braid-like rewind mechanic.
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