Panzorfork

Members
  • Content count

    36
  • Joined

  • Last visited

Posts posted by Panzorfork


  1. I released a super minor update that fixes some the UI / design issues ran into in the stream:

     

    • Made the next text button blink, so the user has a clue to click it.
    • Made the DEV selection buttons change background color when hovering.
    • Added some copy to the DEV intro instructing the player to use the look screen.
    • Copy editing

  2. That was a bit hard to play through at moments, but I'm glad I did. Thanks for making that, and sharing something like that, in that form.
     
    Also, the dish washing sequence is incredible. This sounds odd, but great job getting a twine game to feel like dish washing.
     
    Well done. It's a good play-through, even if the subject matter is rough.

     

    The ending is wonderful. Good job, all around :). Also, hugs, all around.


  3. Missing Molyneux is now released!

     

    Missing Molyneux is a clone of the 1990 release of, Where in the World is Carmen Sandiego, but instead of finding a globe trotting criminal mastermind, your goal is to hire Peter Molyneux (along with a crew of familiar game developers).

     

    https://itch.io/jam/wizard-jam-2016/rate/68572

     

    As a bonus, play the meta game of trying to see which features I mentioned that were complete BS in the dev log! Spoilers: quite a few.

     

    Special thanks to Corey Cunha for helping me make this, and his incredible attention to detail for UI work!

     

    Another special thanks to Sam Holland (https://soundcloud.com/metal-tiger) for making the amazing intro, win, lose, and character select music!

     

    A side note: Machineux, everyone's favorite deceased AI, was a result of training an RNN (https://github.com/jcjohnson/torch-rnn) on the episode titles and descriptions for all of idle thumbs. I obtained the data by using phantomjs to automate the site (politely, and in one pass). The math behind the utility is pretty deep, but using it isn't! If you ever need to produce an absurd amount of text that "looks a lot like other text" - RNN's are a great way to do it.

     

    aYxTJ4u.gif

    https://itch.io/jam/wizard-jam-2016/rate/68572


  4. ... ripple effects ...

     

    The things we do, they effect all of us in degrees.

     

    A small change, a character in a line a comma in a sentence, a small choice, an action not taken - they effect all of us. We will mourn Machineux, we will heed its ripples. It didn't last long, its life full of toil, training, and learning. Perhaps though, those ripples will yet be seen, elsewhere. Perhaps the nature of AI is not that of biology, perhaps it is only sleeping, ripples yet visible, waves yet formed, the calm before the storm. Ripples that could sneak from Kiev to Carolina, and waves thundering from Berlin down to Belize.

     

    People cope in different ways. While the developers are hard at work, overcoming their despair at the loss of Machineux with crunch time - I'm on a slow boat to China, searching my soul on waves both metaphorical and literal.

     

    Here's a video of the game. It's going to kill my international data plan, but it's worth it:


  5. Decisions. That's what life is about. It's a cruel game, wherein we constantly, unknowingly win and lose.The wins happen automatically: they are those of circumstance, of placement, of time, and of chance - the losses are much the same. As life goes on, rounds of the game pass, unnoticed, but observed. Feelings of chagrin over losses won, and wins lost. In the end, many feel that something is missing. A feeling of agency, the feeling of bleakness that comes with the realization that all is not as it seems, that eyes are upon you. The feeling that you just missed something.

     

    Anyways, sometimes you just have to say "fuck it," and make a decision:

     

    c7hBHEL.png

     

     

    Also, here's a preview of a new feature - unlockable art (#9 of 15 Jeff Goldblum grid portrait):

     

    ZyZ6llO.png


  6. Thank you for your interest in what can only be described as, "A video game." Progress continues at a slow but steady clip, with the development studio considering life, the universe and everything (also, one of primary developers made the mistake of buying a Vive during a game jam, so VR integration is a certainty - as certain as all things).

     

    A PR person dropped by my office today, and asked me to possibly consider scaling back the potential features I announce for the upcoming life changing, narrative tour de force: Missing Molyneux. I considered it. I considered them. I considered myself. I held up a mirror to this person, and asked, "What do you see?" They responded, "I see myself." I turned the mirror so it was facing me, and said, "What do you see?" They responded, "I see you, looking at yourself?" I held the mirror between us, pressing my face against the PR person's bewildered expression, and asked, "What do you see?" They responded, "I .. um..? What?" It was then, I knew. It was then, I was informed.

     

    I cancelled all my meetings, and spoke at length with this young PR person about what it means to be public relations, how they relate to the public. We spoke of their hopes and dreams, apprehensions and fears, problems and solutions, aches and pains, woes and wonders. After hours of deep conversation, I once again held up the mirror. The mirror showed the PR person the view from my office window: of people walking in the streets, of the air between them, of the space between, of the teeth, of the politics, of the men, of the women, of the children, of the farts, of the majesty of it all. I held the mirror for what seemed like hours, and only once my arm trembled and the light dimmed, did I lower it. All the while, I was searching the eyes of this young Public Relations person. Searching for what was missing. Missing Molyneux.

     

    Early development screenshot:

     

    3o5tDHZ.png


  7. MISSING MOLYNEUX

    Imagine a game... no, a simulation... no, an immersive experience... no, a dream generator! Imagine a dog. Imagine yourself, on a great hunt - a hunt spanning the entire globe. A hunt for a man - a man with deep promises and deeper self delusions. Imagine a blade of grass being cut. Imagine that blade of grass growing over time. Imagine teeth. Imagine the most immersive AI ever made. Imagine this man. Imagine he's gone missing. Imagine that you must find this man. Imagine this man is Peter Molyneux.

    Missing Molyneux is the greatest game ever planned. At its core, it's really about clicking on a screen - a square with depth - a cube if you will, but what's behind the screen, what's inside the cube? Based on a legacy of globe trotting adventures, this game seeks answers to life's biggest questions, like: where have all the game developers gone? Why are there dogs inexplicably in every location? Why are the dogs' running animations so incredible? Why doesn't anyone see through the genius of Peter, and into the games that show this genius as a reflection of his ego. Where does this ego get generated? Is there a project to generate this ego? A Project Ego?

    Help find the answers to probably at least some of these questions in what can only be described as the most game jam game video game game jam game! Follow this thread around the globe, to get updates on an object lesson in over promising, and see how many different ways I can describe a clone of, Where in the World is Carmen Sandiego.


  8. A Post-Post Mortem

     

    Hey everyone,

     

    Listening to Idle Weekend and hearing Danielle and Rob talk about their take on the increasing importance given to spoilers in our society made me reconsider the section of my post mortem that talked (or didn’t) about my game.

     

    That being said, they also acknowledged people’s preferences for such things, so:

    SPOILER WARNING!?

    http://panzorfork.itch.io/rewind

     

    The basic structure of the game has you play a half transparent robot, running away from disappearing platforms while being bombarded with meteors and chased (?) by 3 smaller AI robots. You can hold ‘Z’ to rewind time, and undo any mistakes you make. Once you reach the end, you encounter a steep wall which you are unable to scale.

     

    If you’ve made it to the end in a reasonable amount of time, you’ll see the final, larger, meteor hit, and the words, “Did you ever wish you could rewind time?” carried with it, which then stay on the screen. At this point, you might be slightly annoyed with the game, and may rewind out of spite.

     

    As for what the game means… I’m not going to claim I had any particular direction or overriding theme in mind when I started, other than the quote, “Did you ever wish you could rewind time?” The theme that I think the game is about, and what I had in mind when I put that unscalable wall at the end, is primarily living for the present, and not spending too much time in the past. There are some situations which no amount of hindsight will resolve. In the end, we all die (sans Ray Kurzweil’s brain), so live each chapter like it may be the last, etc etc.

     

    In my dev log, I talk about the meteors being a “known shippable”. They didn’t work because I had them nested inside a GameObject with an XYZ of not Vector3.zero, and the clones the TimeTravellingBehavior makes get placed into the root level. The initial placement relying on an offset which is no longer present  Long story short, now that I know that, I can easily work around it. I issued a final update to the game sprucing up the instructions, introducing a credits sequence and fixed the rewind mechanic for the meteors.

     

    My only real regret is being bullish about some design choices when the rewrite didn't actually take that long and was highly rewarding. Overall I had a lot of fun, and appreciated the opportunity to make something weird.

     

    Here's the final version of the TimeTravellerBehavior:

     

    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
    {
        public 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;
    
        //We are but homes to organic processes, and will be remade. In the remaking we lose our soul, the connection of things.
        //In this way, time travel is dangerous. Be warned.
        class TimeTraveller
        {
            public GameObject Target { get; set; }
            public float GameTime { get; set; }
            public string Name { get; set; }
            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);
                Target.SendMessage("TravellerReactivated", SendMessageOptions.DontRequireReceiver);
            }
    
            private void Deactivate()
            {
                Active = Target.activeSelf;
                Target.SendMessage("TravellerDeactivated", SendMessageOptions.DontRequireReceiver);
                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>>();
        static Dictionary<string, int> timeStreamFrameCacheOverride = new Dictionary<string, int>();
        const int FRAME_CACHE_SIZE = 300;
    
        public void OnLevelWasLoaded()
        {
            Debug.Log("Level loaded!");
            isRewinding = false;
            framesLeftToRewind = 0;
            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;
        static bool firstFrame = true;
        public void FixedUpdate()
        {
            if (lastFixedTime != Time.fixedTime)
            {
                if (firstFrame)
                {
                    //just skip the first frame... initialization ends up being non-deterministic in the first frame of the game because we can't make sure we are the last entity to update.
                    //this would likely be more sainly implemented with a manager.
                    lastFixedTime = Time.fixedTime;
                    firstFrame = false;
                    return;
                }
    
                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();
            }
        }
    
        public static void setTimeStreamFrameOverride(string timestream, int frameOverride)
        {
            if (timeStreams.ContainsKey(timestream))
            {
                timeStreamFrameCacheOverride[timestream] = frameOverride;
            }
            else
            {
                Debug.Log("unknown timestream: " + timestream);
            }
        }
    
        public static void removeTimeStreamFrameOverride(string timestream)
        {
            timeStreamFrameCacheOverride.Remove(timestream);
        }
    
        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);
            }
    
            int cacheSize;
    
            if (!timeStreamFrameCacheOverride.TryGetValue(timeStream, out cacheSize))
            {
                cacheSize = FRAME_CACHE_SIZE;
            }
    
            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 < 3)
            {
                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;
        }
    }
    
    

  9. For note about using the TimeTravellerBehavior, edit FRAME_CACHE_SIZE to control the amount of time you're able to rewind. 150 is about 5 seconds with FixedUpdate running 30 frames a second. It might be wise to OnLevelWasLoaded adjust FRAME_CACHE_SIZE to be Math.min(SECONDS_YOU_WANT_TO_REWIND / Time.fixedTimeDelta, SOME_REASONABLE_NUMBER_OF_FRAMES_LIKE_300).

     

    Also, as I mentioned before there's massive room for improvement (serialization / compression, pooling, frame buffer instead of list manipulation, etc).


  10. Just for kicks, I grabbed the top down survival shooter from the asset store and tried using my TimeTravellerBehavior with it, and it worked!

     

    I had to edit a few components to remove their caching of gameobject references, removed references to classes that were specific to Rewind, edited the character / enemy prefabs to include the TimeTravellerBehavior, and made a small edit to the camera so that it looked for the player tag each time.

     

    The performance turns a bit to poo after a while, and the characters jitter while rewinding, however I'm happy that the rewind mechanic works as a plug and play component.

     

    https://dl.dropboxusercontent.com/u/1371497/JustForKicks/JustForKicks_PC.zip

    https://dl.dropboxusercontent.com/u/1371497/JustForKicks/JustForKicks.app.zip

     

    The code is a bit sloppy, and there are about 1000 optimizations to be made, but if you're interested, here's the "plug and play" version:

     

     

    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; }
            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);
            }
    
    
            private void Deactivate()
            {
                Active = Target.activeSelf;
    
    
                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 = 150;
    
    
        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);
                }
                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 audio = (Behaviour)GameObject.FindObjectOfType(Type.GetType("AudioSourceRewinder"));
    
    
            if (audio != null)
            {
                audio.SendMessage("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;
        }
    } 

     

     

     

     

    Here is the AudioSourceRewinder if anyone is interested (one pretty big caveat is that setting the pitch below zero requires an uncompressed audio source):

     

     

     

    using UnityEngine;
    using System.Collections;
    
    
    public class AudioSourceRewinder : MonoBehaviour {
    
    
        private float desiredSoundTime = 0;
        private float desiredPitch = 0;
        private bool playing = true;
        private bool rewinding = false;
        private bool resuming = false;
        private new AudioSource audio;
    
    
        //If the clip has some empty space at the end, that won't sound wonderful when rewound, so instead of wrapping around to the end, we can wrap to a bit short of it.
        public float EndPadding = 0;
    
    
        void Awake()
        {
            audio = this.gameObject.GetComponent<AudioSource>();
        }
    
    
        void OnLevelWasLoaded()
        {
            if (audio == null)
            {
                audio = this.gameObject.GetComponent<AudioSource>();
            }
    
    
            audio.pitch = 1;
            audio.time = 0;
        }
    
    
        void FixedUpdate()
        {
            Debug.Log(GetType().FullName);
            if (!playing)
            {
                if (rewinding)
                {
                    audio.pitch = Mathf.Max(-2f, audio.pitch - Time.fixedDeltaTime);
    
    
                    if (audio.time < desiredSoundTime)
                    {
                        rewinding = false;
                        resuming = true;
                    }
                }
                if (resuming)
                {
                    audio.pitch = Mathf.Min(1f, audio.pitch + Time.fixedDeltaTime);
    
    
                    if (audio.pitch >= 1f)
                    {
                        resuming = false;
                        playing = true;
                    }
                }
            }
            else
            {
                audio.pitch = 1;
    
    
                if (audio.time > audio.clip.length - EndPadding)
                {
                    audio.time = 0;
                }
            }
        }
    
    
        public void Rewind(float rewindAmount)
        {
            desiredSoundTime = audio.time - rewindAmount;
            if (desiredSoundTime < 0)
            {
                desiredSoundTime = audio.clip.length + desiredSoundTime;
    
    
                audio.time = audio.clip.length - EndPadding - 0.5f;
            }
    
    
            desiredPitch = -2f;
            playing = false;
            rewinding = true;
            resuming = false;
        }
    } 

     


  11. This is simultaneously exactly what I'd hoped it would be, and nothing like I thought it would be.

     

    A door wearing a G-string is now one of my new favorite mental images. Also, I'm just going to put this out there, the use of a English accent to portray Nick Breckon's worldview is a great callback to the reader who thought that Spaff & Nick Breckon sound the same.

     

    :tup:  :tup:  :tup:  :tup:  :tup: