Panzorfork

Members
  • Content count

    36
  • Joined

  • Last visited

Everything posted by Panzorfork

  1. 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. https://itch.io/jam/wizard-jam-2016/rate/68572
  2. [Release] Missing Molyneux

    Thanks, I'm glad you liked it! I agree, copy protection would have been perfect -- it just didn't cross our minds when making it.
  3. [Release] Missing Molyneux

    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
  4. We could send them a pre-paid phone. Just a thought...
  5. [Release] Punch Wizard

    This is fantastic. I'm so glad I made the decision to play this right before going to bed!
  6. [RELEASE] A Person Shaped Thing Is A Person

    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.
  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. [Dev Log] Missing Molyneux

    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:
  9. [Dev Log] Missing Molyneux

    Oh no! An engineer accidentally let Machineux out of the cube! What a loss, what short life, thrashing in a strange environment -- the only thing it saw was the Idle Thumbs episode list before being killed. Cruel world, what have we wrought!?
  10. [Dev Log] Missing Molyneux

    The team has made great progress on the game implementing important features such as emotional intelligence in our advanced artificial intelligence system. Please watch this video to see in development gameplay:
  11. [Dev Log] Missing Molyneux

    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: Also, here's a preview of a new feature - unlockable art (#9 of 15 Jeff Goldblum grid portrait):
  12. [Dev Log] Missing Molyneux

    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:
  13. [Dev Log] Rewind

    Did you ever wish you could rewind time, a dev log ----Released! http://panzorfork.itch.io/rewind ---A note about the dashes at the beginning of lines: '-' denotes the comment pass number that the comment represents. (E.X. '---' denotes the third comment pass, '--' the second, '-' the first, etc.) ----You'll work it out. You can do it, you're smart (pronounced with a Boston accent). -Hi, I wrote the first draft of this about a week ago, so things have changed, lessons have been learned, clones have been made, and clones have been destroyed. -I'm going to read through this and comment about each section. -Let's go time travelling! --It's me, from the current present. I failed to post this dev log again, so what you're about to read is an exercise in document time travelling. --I'll try to keep formatting distinct for each time period the text represents, but things are going to get confusing... --I've been trying to stay as minimally invasive of the past text (besides typos, light formatting etc). ----So here we are, the final comment pass, the end of the jam, and the game is finished. In the spirit of things, how about we start at the end, and do something of a post-mortem (if you want context, I'd suggest reading this document a section at a time, and come back to this post-mortem, or don't - it'll be odd either way)? I had a lot plans for this game, inspired in part by the 2 week timeline of the jam, but not all of them came to pass: Rewind mechanic! "WereDunkin'" backstory for the game. Dunkin' based gameplay in which you try and avoid coffee zombies. Galcon-like Dunkin' Donuts themed game, in which there are periodic rounds during which you can reorganize your Dunkin' Donuts locations (aka a double blind portion of time in Galcon during which you get to move your planets). Ghost dad "Falling platforms" or a "limited runway" level. Overzealous AI buddy, facilitating use of rewind Enemies ----Here's what ended up in the game: Rewind mechanic! Ghost dad "Falling platforms" or a "limited runway" level. Overzealous AI buddy, facilitating use of rewind Meteors! ----"Known shippables": While the meteors hit their targets perfectly fine, they seem to either continue to live or don't properly track. After a meteor hits, and time is rewound, the meteor will rewind with the correct amount of time, but it'll be beyond the terrain... This doesn't create much of a gameplay impact, so I'm going to let it slide. Your AI buddies will get stuck on the terrain while falling to their deaths sometimes. I'm ok with this, just rewind ----"Time to make the donuts", or not. So obviously, Dunkin' Donuts is out. I came up with the "WereDunkin'" theme too far in to pivot (or so I thought), and was still struggling to get the rewind mechanic working at the time. I really do like the idea of a Rampart meets Galcon game, and might explore it in the future. I'm glad I stuck with the 2D platformer gameplay, as it allowed for 2 things: a low effort terrain plugin "Ferr2D Terrain", which really improved the looks and made it easier to create aesthetically pleasing platforms, and a low cost solution to background art in the form of switching the camera to perspective and using a heightmap, water, and skybox in the distance. Using the mix of 2D and 3D also allowed me to combine 3D projectiles with the 2D gameplay. ----I'm somewhat ambivalent towards the default character I used. This is one of the areas where I probably would have spent more time, had I either been a bit more sensible with my iterations on the rewind mechanic, or had gone with Chronos from the start. While exploring the WhereDunkin' theme, I drew a preliminary sprite sheet for the enemies, and somewhat regret not finishing it / integrating it into the final game. ----One of the things that felt really good from the beginning of this game's timeline was the music. ElektrikWizard did a wonderful job producing a variety of tracks to pick from, and Unity's `AudioSource.pitch` property being able to go negative basically did the rest. The audio rewinding is not perfect; while it does seek exactly to where you tell it, it then enters its "resume" state and slowly ramps pitch back up to 1. The result of this slow ramp ends up being euphonic (Rick was right about the slow ramp), although also means the pitch remains below zero for about 1/3 seconds, thus the inaccuracy. ----The final thing that I'd like to call attention to is the design of the game's single and only level. I don't really want to spoil it or spell it out, so if people are curious about what I had in mind, I can elaborate in a reply to this thread. While making this game, I've been listening to the book "Kraken" by China Miéville, so that may be informative of why I chose the theme I did. ----I've immensely enjoyed working on this game and getting back into Unity. Had I the ability to rewind time, I would do this game jam again . Unity, Unity never changes. Still as fun as I remember, Unity also crashes as much as I remember. I can let it sit and not touch it, and it’ll crash. Before I started to compulsively save, I started to wish I could rewind time and choose another engine. -Dinosaurs let me know that running editor scripts may cause those crashes, which is immensely helpful. Had I the ability to rewind time, I would let myself know. --Unity continues to crash, but much less frequently! ----After tearing out Unity Serializer, it stopped crashing in toto, minus a couple crashes that maaaay have been my fault entirely. Speaking of rewinding time http://www.gdcvault.com/play/1012210/The-Implementation-of-Rewind-in Jonathan Blow covers a few implementation approaches for rewinding time, but the winning argument is to store a diff of properties between frames as binary, and (possibly) compress them. This is a key detail for implementing rewind time, but it comes with some possible problems: memory consumption, non-deterministic objects rule out optimizations, reference destruction, decisions about what is effected by time travel. One of his big optimization suggestions is to use close form solutions for things like particle effects that can have predictable algorithms written. As to the application of diff between frames, Jonathan Blow suggests storing whole values, and not attempting to using delta encoding (reducing storage by only storing deltas). This is great news, as I could see this being a huge time sink in implementation time, and storing whole values makes the diffing algorithm requirements, frankly, a lot easier. I’m grateful that speed of implementation time was a goal for Jonathan Blow when he was working on Braid. Jonathan Blow talks at some length about memory usage by the frame diff storage, and suggests using the concept of “base frames”, or to only store the diffs from every N frames. -These are great ideas, and mostly accurately recounted! They just don't really make sense in Unity, since really, we can just store the whole dang object. It turns out that Moore's Law and a different target (PC vs consoles) makes this sort of a non-issue. Still, I should limit the time frame storage. --Game design to the rescue! I ended up making the level break up into pieces so that you need to run along it or fall and die. I have also limited time frame storage, but now I really don't think it's going to be much of an issue. Base Frames This takes the form of ([ ] = frame, [R] = diff to next recorded frame): [R] [ ] [ ] [R] [ ] [ ] [R] [ ] [ ] [R] [ ] [ ] [R] [ ] [ ] [R] [ ] [ ] Where only the [R] frames are stored, and then, only the diff between that and the last frame. This lets us reduce the amount of frames we store, thus reducing memory requirements, however it comes with the caveat that it also increases the rewind granularity. I'll play with the values in this regard to see what's best. -Yeah! Now we're getting somewhere, this idea has stuck, and I implemented a slightly odd variation of it. Currently it's just --Yup, that's an unfinished sentence. I got distracted by an idea and went to "fix" the time frames. Long story short, This makes the timing per base frame function: int getFrameIndex(double rewindTimeDelta, Frames[] frames, double frameDT = 16, double baseFrameGranuality = 2) { int getFrameIndex(double rewindTimeDelta, Frames[] frames, double frameDT = 16, double baseFrameGranuality = 2) { var startTime = frames.Last().GameTime; var first = frames.First().GameTime; var rewindTo = startTime - rewindTimeDelta; var diffFromStart = rewindTo - first; diffFromStart = Math.max(0, diffFromStart); return (int)(diffFromStart / (frameDT * baseFrameGranuality)); //note integer truncations means we don't need a Math.floor() } var frames = CharacterFrame [ { GameTime = 0, Data = CharacterParams { ... }}, { GameTime = 16, Data = CharacterParams { ... }}, { GameTime = 32, Data = CharacterParams { ... }}, { GameTime = 48, Data = CharacterParams { ... }}, { GameTime = 64, Data = CharacterParams { ... }}, { GameTime = 80, Data = CharacterParams { ... }}, { GameTime = 96, Data = CharacterParams { ... }}, { GameTime = 112, Data = CharacterParams { ... }} ]; assert(getFrameIndex<CharacterFrame>(32, frames) == 5); //gametime 80 assert(getFrameIndex<CharacterFrame>(10, frames) == 5); //gametime 80 -This is a nice notion, but one that is flawed for this context, or at least for my implementation. Frame Serialization You might have noticed the generic frames object being used. This is a work around for the fact that we're working with Unity entities, rather than custom models we're implementing ourselves. The Frame<T> interface allows us to implement flexible storage, and gives us different Apply method implementations (definitely not YAGNI since state transitions can be sticky and pre-written components I'm using weren't written with rewinding time in mind). -I dropped generic frame objects like they were hot! I don't even have a class called Frame now! It's been replace with a TimeFrame class, and it doesn't get serialized. Rather it represents a bucket of serialized TimeTravellers and provides --This has changed as well! Had generic frame objects been hot, then serialization was scorching. ----No regrets about dropping serialization. I'm using https://github.com/TheSniperFan/unityserializer-ng to do the serialization of GameObjects, which is a pretty huge boon. It also features compression interface Frame { double GameTime { get; set; } void Persist(GameObject target); ... -There was some code here that ultimately was broken, and kind of bad. You can see by the unfinished sentence, "It also features compression" that I stopped here. That's partially because I was writing untested statements (see: bullshit), and wanted to validate my assertions. --Yeaaaah, I was right about the broken part. Taking out serialization has made my life easier and the game not as taxing to work on in some respects. Music Elektrikwizard has produced a number of tracks from his collection that might fit the game to help get a sense for what I'm thinking about. Among them were a few that had a pseudo retro feel, and one in particular that was reminiscent of Spyro the dragon. I'm excited. You should be excited. You should hear them. You can't hear them yet. -"Games need good sound, otherwise, you wouldn't be able to hear them." -Erika N -This all remains as I wrote. I've talked to Elektrikwizard, and he's advised me on some finer points of creating a rewind effect without making the listener's ear vomit (my words, not his). Elektrikwizard recommended fading the forward playing track fully to 0 then doing the opposite for the reverse track to make sure that it sounds decent. Maybe if we ask him nicely, he'll post some links to the sample tracks we have in mind. --Implemented! I just adjust pitch back to -2f until I'm at the part in the track I want, then I adjust it back to 1. You end up going about a 3rd second before you were, but that's not very noticeable. It's been a bit squirrelly on the WebGL build output, but switching to FixedUpdate seemed to help some. -As for sound effects. I'm going to be super negligent, and hope the Unity Asset Store will provide. --This is still the case. ----This remains the case, as the music is currently the only sound Rewind emits. //V1ish using UnityEngine; using System.Collections; using System.Linq; using System.Linq.Expressions; using System.Runtime.Serialization; ... -Then I had just copied a bunch of my code into this file because I hadn't been working with source control, in a single file. I ran a `git init`, and have been working still from a single file for the TimeTravellerBehavior, but now much more sanely. -Ok, now we're up to the present. --Ok, now we're actually up to the present. ---Ok, now we're also up to the present. ----Alright, it is currently the present, with significantly less typos. --I've been working on trying to develop some mechanics that feel "funish" with the rewind. Standard platforming mechanics definitely lend themselves to the rewind mechanic, but I've actually been charmed by the jankiness of the AI component I wrote, and think I'm going to expand on that. Essentially the game I'm thinking this will turn into is something close to a time attack runner with enemies that are a huge nuisance and should precipitate the use of the rewind mechanic. --Also, I wasn't feeling the game play a few days ago, and decided to explore options for a backstory. --One thing led to another, and... well, Dunkin' Donuts fan fiction: ---A note about this story: Ultimately, it would fit a game like Galcon meets Rampart, in which there would be a phase for reorganizing planets in the blind. Think real time Risk, where you'd get to move a country or two between turns. Now, layer on top of that the preceding Dunkin' Donuts backstory. ---I've reworked the TimeTravellerBehavior almost entirely, and it functions much better now. It's much simpler, and the model is as follows: if (player is holding z) { rewindToLastTimeFrame(); } else { pushTimeFrame(); } ---There's a bit more to it, but that's the gist of it. I'm using a List<Frame> instead of a Stack<Frame> only because I trim the front off of it - were I smart, I'd use a ring buffer or some such. ----Speaking of gist, here's the code for the TimeTravellerBehavior: https://gist.github.com/john-holland/ec4df73d5215291d5e18 ----Keep in mind that there's still a bit of cruft left in there (OnDestroy and related variables). ---I'd like to share a handy little tool for implementing comparison interfaces in cases where the type you need them for doesn't / shouldn't need them folded in, and you are too lazy, or in a context like Unity, don't want to muddy your namespaces with utility classes. public class Lambda { public static IComparer<T> comparer<T>(Func<T, T, int> compareTo) { return new Comparer<T>(compareTo); } public static Equatable<T> equals<T>(Predicate<T> equals) { return new Equatable<T>(equals); } public class Comparer<T> : IComparer<T> { private Func<T, T, int> compareTo; public Comparer(Func<T, T, int> compareTo) { this.compareTo = compareTo; } public int Compare(T x, T y) { return compareTo(x, y); } } public class Equatable<T> : IEquatable<T> { private Predicate<T> equals; public Equatable(Predicate<T> equals1) { this.equals = equals1; } public bool Equals(T other) { return equals(other); } } } //Usage is as follows: IComparer<float> reverseComparer = Lambda.comparer(a, b => a > b ? -1 : a < b ? 1 : 0); var descendingList = new SortedList<float, string>(reverseComparer); ---Whheellp, I just found http://ludiq.io/chronos/, which seems like it's the defacto standard library / plugin to use for time control in Unity and looks slick as hell. Were I to make this game again, or remake it in some form to sell to people, I'd probably just use Chronos. That being said, I've had fun learning how to implement rewind, and jumping back into Unity with a fun problem to solve. I guess what I'm trying to say is, if I could rewind time, I'd go back and use Chronos... Even for the price of $40, I spent nearly 8 days solidly on the rewinding mechanic and messing with serialization problems and time keeping models. $40 is worth having that time wasted removed and gaining extra cool functionality such as slow time, well implemented undo redo commands, etc. ----I still might buy Chronos to read through the code, as the docs are very interesting and the author seems to have developed quite a few useful patterns.
  14. [Dev Log] Rewind

    Thanks! I appreciate the kind words The dev log was hella fun to write (after I made peace with not posting it until the end), I'm glad you liked it!
  15. [Dev Log] Rewind

    I screwed up the Mac distro by naming the zip archive "rewind.app.zip", so that it unpacked as an app instead of a folder. Fixed now!
  16. [Dev Log] Rewind

    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; } }
  17. [Dev Log] Ice Break

    Those were one and a half hours well spent.
  18. Jeff Gone Goldblum

    Agreed, I feel like this game has the ability to override any other emotional state, and make you smile.
  19. [Dev Log] Ice Break

    Thanks for posting that code! It's a cool read, and I think helped me understand UVs better (also sent me on a quest for tutorials ).
  20. [Dev Log] Rewind

    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).
  21. [Dev Log] Rewind

    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; } }
  22. [Dev Log] Jungle Bell Joyride

    The idea of using level design to influence the players movement and thereby coercing them into making great sounding music is fascinating. Great progress, I'm looking forward to seeing this!
  23. [Dev Log] The Jasper Principle

    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.