Time travel in Unity | Useless Game Dev

Поделиться
HTML-код
  • Опубликовано: 12 ноя 2024

Комментарии • 102

  • @znoshim
    @znoshim 9 месяцев назад +63

    I worked in a game for a game jam last week and one of the mechanics was time travel. We implemented a much easier system, as we only had to modify time in one scene with a set length of one minute, and we just used timeline for doing it. But watching your video has motivated me to take this approach in future projects that have this kind of mechanics. Also, as an engineer, I really enjoyed the optimization you did on the recorder algorithm. Congrats!

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад +17

      Interesting, how did it work with timelines exactly, I assume you wouldn't have any dynamic object then? Just a "film" on the timeline that you can rewind/fast-forward? Interesting nonetheless for a narrative game!

    • @znoshim
      @znoshim 9 месяцев назад +9

      @@uselessgamedev exactly! it was a pre-designed scene using the timeline, there were few elements that moved through time, such as npcs, and you could control the quantity of time that had passed via script (miles away of the system you've developed, but an easier approach for such a time limited event as a game jam is, nonetheless)

  • @MattyDoesGameDev
    @MattyDoesGameDev 9 месяцев назад +25

    I'm imagining how exponentially more complicated this can get when you include objects that change states, for example a barrel that explodes! You'd have to design each gameplay object to have time-travel in mind. Thanks for the fun video, as always!

  • @RGHdrizzle
    @RGHdrizzle 9 месяцев назад +11

    I was just planning to implement a time rewind system as a small project and here u go with an upload exactly about that which also gave me some insights on how to do it

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад +2

      As other have suggested, if you don't need arbitrary access, consider using a linked list!

  • @alex2416
    @alex2416 9 месяцев назад +5

    i always wondered how they made this work on a Switch for TOTK. Thanks for exploring this question, great video ! :D

  • @REVYMofficial
    @REVYMofficial 9 месяцев назад +8

    Your design also shows the potential for in-game replays. Great Work!😄

  • @xeetsh
    @xeetsh 9 месяцев назад +4

    Great video! Especially great that you showed how to optimize this. Most tutorials are lacking this step.
    I made a game jam game once that used basically the same technique, not for rewinding, but for playing certain actions back. Your goal was to catapult a car to a certain point with conveyors, explosives and oil spills by placing those objects, seeing where the car would land, resetting the car and continuing to build your track from there. As Unity physics are far from deterministic, I needed a way to record what happened before the reset in order for the car to land where it landed last time. Not the best thing to have to deal with during a three-day game jam, but I managed to do it basically exactly how you managed to record and rewind movement of game objects in your video (just way less performant)!
    The game is Wildlife Camping Experiences (on itch) if anyone is interested :)

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад +1

      That sounds cool! I guess this is more akin to a save system to be able to restore the game at checkpoints but that's interesting nonetheless!

  • @BobsMudHut
    @BobsMudHut 9 месяцев назад +7

    Great video! I've been working on a game with time manipulation for over a year and still use lists to store all my rapidly changing data (like transform positions) because I haven't gotten around to optimizing everything yet 😅. For things that don't change very often or on many objects like changing animations or interactions between timed objects I use a linked list that records forwards/backwards functions with any needed state parameters to save on space. Writing the functions can be a little complicated sometimes but it means I can record any arbitrary action I want and it doesn't have to be dependent on constantly storing/checking state every frame like when recording transform positions. When things get really fun (read: complicated) is when you have different objects rewinding/replaying at different speeds/directions in time.

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад +2

      Linked lists are a great solution indeed!

    • @stickguy9109
      @stickguy9109 9 месяцев назад +2

      This video instantly reminded me of you

  • @alejo460
    @alejo460 9 месяцев назад +3

    Useless game dev but every single video is the most useful and talented game dev video possible

  • @DrEnzyme
    @DrEnzyme 9 месяцев назад

    It's amazing how easily you can get cool mechanics out of a system like this when you have the base implementation. You can have objects that are immune to time travel just by deleting their recorders. The objects that lose momentum when they're unfrozen in time can be a game mechanic. You can have multiple copies of your player that appear every time loop, leading to fun interactions and paradoxes.
    Time travel is sick.

  • @designator7402
    @designator7402 9 месяцев назад +2

    A little while ago I saw a video about cursed units of measurement, and I think I will add "Frames per Frame" to my own personal list of cursed measurements. Thanks, funny blue turtle friend.

  • @guillaumemagniadas2472
    @guillaumemagniadas2472 2 месяца назад

    Hello ! Very nice video ! Just a little comment about the map you're using, I'm mostly a C++ developer, but I'm sure it should be the same in C#, classic map datastructure keys are stored sorted (due to the format of the hash it's using), so their should be directly a method for you to get the element before or after the key you're looking for in your map, in C++ the method is called map::lower_bound for example

  • @williefr
    @williefr 9 месяцев назад +1

    What an awesome video! I love when we can optimize stuff. I also learned a time ago these algorithms and just forgot them as I'm not using them at work. Also very well explained and a lot of work put in. New subscriber 😊

  • @AdamMelvins
    @AdamMelvins 9 месяцев назад

    Awesome video! :) I'd like to note that your "simple scene" is utterly beautiful! :) I love that art style, the distance fade - it looks gorgeous and makes for a really nice video to watch. :)
    I've been working on a "replay system" for work, and thankfully it's not got anything with physics, just player positions, the task/quest, and what's on the UI and the positions/visibility of objects. Even that... took a LOT of work :) You're a wizardly wizard, and that's a really clean implementation! Thank you for making something near and dear. :)

  • @Queue_Bert
    @Queue_Bert 9 месяцев назад

    My Team is actually working on a 3D puzzle platformer about time travel right now! I'm the animator, not the lead programmer, but from what I can tell your implementation of rewinding and recording is similar to ours but a fair bit more complex and versatile. We're mainly recording the player's position rotation velocity, and then playing it back as "clones" the player can then interact with. For at least my animations I wish we could go back and rework the systems to take advantage of some of the ideas you brought up in your video here, but I'm not sure how possible it's gonna be this late in development 🫠. Either way, this was a great video as always, keep up the amazing work!!

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад

      Ah, good luck! Looking forward to a new time travel game!

  • @amarok8bit
    @amarok8bit 9 месяцев назад

    What a great video! I also like games with time manipulation like Braid. Last year I wrote my game Time Wizard for Atari 8-bit computer. You can see it on my channel. I fully understand problems related to memory management for storing and restoring gameplay. In my case I had 48kB of RAM and CPU with 1.77 MHz. So this is completely different environment but the same challenges. Thanks for sharing. :)

  • @omnisel
    @omnisel 9 месяцев назад

    Binary searches are so cool !!! Always the best way to search an arbitrarily large ordered list.

  • @slice6298
    @slice6298 9 месяцев назад

    Great idea, yet relatively simple to implement
    Really shows that you don't need to be incredible at programming to be able to create games with fun systems

  • @newe6000
    @newe6000 9 месяцев назад +5

    I'm surprised nobody has mentioned how this approach naively assumes that a frame is a fixed length of time, instead of being dynamic. To be fair it works much better than I expected in this prototype, but once you put this in a more complicated scene with framerate fluctuations they're going to start showing up in the recordings.
    A more ideal solution I think would record at a fixed framerate (maybe every second fixed update?) then interpolate between those frames on playback. Though I understand avoiding that complexity in a first draft prototype.

  • @JamesTM
    @JamesTM 9 месяцев назад

    Really cool video, as always! Very interesting.
    One possible, significant optimization you could consider:
    For the rewind/ff, I dont think you need to access an arbitrary frame. You only need to acces the frame either immediately before or immediately after the "current" (last recorded/restored) frame. Or at least within a very small number of frames, if you're skipping some. But never thousands of records backwards in the list. So, perhaps a doubly-linked list would be the most efficient data structure. That way, you can scrub backwards and forewards through the list without worrying about arbitrary lookups.
    In theory, that would also make deleting "future" frames as easy as deleting the forward link from the current frame record. (I say "in theory" because I don't know anything about Unity's garbage collection.)

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад +1

      That's correct, others have suggested linked lists as well, which is definitely something I should have thought about! I need to try to find an excuse as to why we would need to access any arbitrary frame at any point then

    • @JamesTM
      @JamesTM 9 месяцев назад +1

      @@uselessgamedev One possible excuse would be loading an arbitrary point in time. But you could probably resolve that easily enough with some kind of intermittent bookmark. Maybe a lookup table that provides a link to every 1000th frame? (Though, the garbage collection gets harder again if you do that.)
      Still a stretch, though, since taking a few seconds to find all the correct frames to restore isn't unreasonable for a quick-load, in the way it would be for rewind (which needs to complete with enough time left to render the frame.)

  • @DarthBiomech
    @DarthBiomech 9 месяцев назад

    I had an idea about space strategy game that takes light speed into account and forces the player to play _predictively,_ so this will definitely be useful, thanks!

  • @morgan0
    @morgan0 8 месяцев назад

    i think you could get a further speed increase by storing all parameters for one object in one frame, since most of the time they’d all change together. it might also be worth looking back at the last second recorded, and replacing some frames with a smaller amount of interpolation frames, which could store less data and be simpler to lookup.

  • @beidero
    @beidero 9 месяцев назад +5

    I do wonder if a linked list would be a good method for storing the recorded frames, since you would always start at the last frame and jump backwards. Search would always be 0(log(n)) but you get to skip the conversion to array or list which would save quite some time on larger lists. You could even do like a secondary list that stores references to every 100 recorded frames or so, so you can skip backwards quickly if needed and then just start your search from the closest frame to the frame you are looking for.

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад +4

      Yes that would very much work! Unless we want to do big jumps and skip to arbitrary frames IDs, but with my setup we could only rewind/fast-forward, which even when rewinding at a higher speed (>=2 Game frame per Unity frame) the linked list would work very well.
      That's probably even faster than my solution T_T good job!

    • @beidero
      @beidero 9 месяцев назад +3

      @@uselessgamedev I added something in an edit, storing a second list with refs to every 100/1000 frames would allow for fast big jumps. would be even faster than 0(log(n)). Could also keep a ref to last looked up frame since next lookup is likely next to it

    • @slice6298
      @slice6298 9 месяцев назад +1

      Binary search on linked list wouldn't be log(n), it would be n*log(n), naive would be just n, but it's definitely a lot better idea if you just want a simple time rewind

    • @beidero
      @beidero 9 месяцев назад

      ​@@slice6298 You are indeed right, I was typing up my reply a bit quickly as I had to run earlier. But the more I think about this problem the more interesting it is. I think I would still go for the linked list approach as it has several benefits. But the optimizations you could make are just super fun to consider, if you like a programming challenge that is.
      You could do a lot of lookup optimzations, but also when you consider that not every frame is recorded it becomes more difficult to look up neighbours since an object might not have moved for a long time so how would you optimize for it. I guess you could optimize for worst case scenario but is there a way to do better? For a real game you would need a time cutoff point so maybe you say we record the last 30 seconds. In which case a linked list is fantastic, specially if you do a lookup table of 1 second interval (so I guess lookup table distance is your FPS). You can just cap the lookup table at 30 items and if it ever is above it you just cut off the linked list. So very easy memory management.

    • @slice6298
      @slice6298 9 месяцев назад +1

      @@beidero I've thought of something, you could have an array that wraps around and a value pointing to the beginning. That should be both memory and time performant, although I think time shouldn't be an issue, especially since the game probably wouldn't need to and maybe even shouldn't do part of the updates.
      Also the frames solutions is prone to instable framerate, so having set time different from the game frame is good idea

  • @ibrabdo
    @ibrabdo 9 месяцев назад

    Great video :D Interesting idea, i'll try to apply the same concept in unreal engine for my game 😅

  • @notTryio
    @notTryio 9 месяцев назад +101

    Mayonnaise on an escalator

    • @sushismitcher225
      @sushismitcher225 9 месяцев назад +48

      That's a really interesting and valuable remark on this subject. It made me think of a whole new facet of time travel in video games. Thank you so much. You have changed my life

    • @Youtubershacksshifat
      @Youtubershacksshifat 9 месяцев назад +1

      yay

    • @wittymchitty
      @wittymchitty 9 месяцев назад +13

      Going upstairs, so see you later

    • @sushismitcher225
      @sushismitcher225 9 месяцев назад +1

      be careful not to slip on the mayo@@wittymchitty

    • @Rknife
      @Rknife 9 месяцев назад

      ​@@wittymchittyrotalacsananoesiannoyam

  • @anonymous49125
    @anonymous49125 9 месяцев назад

    converting the keys to array or list also means you're allocating a new array each time you do that... which your GC is going to have a field day and will likely cause a huge lag hickup every 1 second.... with thousands of objects doing this every frame --- wew lad...
    I'm glad with the bst optimization you were able to cut the times down - and if it works, it works - but the approach I would use is having all the recorders be subscribers, recording not to a dictionary but a fixed size array (a list that keeps keyframe for each rendered frame forever is too wild for my blood), and then going back in time, just ask each subscriber if they have something to do for the current frame (a rolling index to keep track of what your current saved keyframe is); if they have nothing going on and are just waiting around then they can check if the last recorded key (that current frame index again) matches the current time or not, if it does, then go back one more keyframe in the array and figure out the distance of time between the current and the previous keyframe - and use that for interpolating between those two keyframes- then when asked if they are busy or not next time (which they are busy interpolating), then interpolate between those two keyframes instead. If you ask them to do something and they don't have anything for this frame and they are not interpolating between frames, then do nothing. this takes your O(log n) - where N are all your keyframes in the tree for that object and brings it down to O(1), just asking "do something for this frame" to each object. I think the benefits are not just performance (where there assuredly would be - doing a bst per frame per object is bananas - and in any case O(log n)>O(1)) but more important, readability. Mucking about with dictionary is nice for that key lookup of O(1) but any type of figuring out where the last keyframe is, makes it a less than optimal strategy.

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад

      About GC, what I ended up doing was caching the array keys, and flagging it as dirty when recording a new frame. This way it is rebuilt exactly once per playback session.
      I agree it's still a lot but that's better than allocating it for every lookup

  • @omayoperations8423
    @omayoperations8423 9 месяцев назад

    There's a great VR game that does something like this. It's called "The Last Clockwinder"

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад

      That was really fun to play there aren't many good VR games but I loved this one. And I love rube Goldberg machines which another topic we can explore in the future ;)

  • @Draugo
    @Draugo 9 месяцев назад

    Personally instead of gaps I would have recorded changed values as a new index and then had a frame array where each frame has the value of the current latest recorded values. Sure you're adding an int to the recorded data but you don't need to deal with gaps and the correct data is easily findable with frame index. For purge you could remake the frame array until the desired spot and then discard all frame data that is higher than the stored index of the last frame.

  • @PingsGolf
    @PingsGolf 9 месяцев назад

    This was a nice video. Well made and interesting topic
    Also i like the music👍

  • @mmertduman
    @mmertduman 7 месяцев назад

    You could also set a fixed time delta for when you record the state! Most of your values are interpolable, and given a small enough time delta, a linear interpolation wouldn't look bad. So instead of saving 60 states per second (or whatever your fps is), just save 5 or 10 and lerp between them.

  • @draakevil
    @draakevil 9 месяцев назад +1

    Awesome video. Very easy to understand the concept.

  • @bungercolumbus
    @bungercolumbus 9 месяцев назад +1

    Wouldn't time.timescale = 0 be a better approach for pausing the game?

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад

      Timescale is generally not a good approach for pausing a game. It works in some cases and is super easy to use so it's fine but more complex projects will want another system for pausing.
      Regardless, in this case it wouldn't allow for rewinding/fast-forwarding so I use the playbackcontroller anyway

  • @AxeLea3
    @AxeLea3 9 месяцев назад

    An interesting game that is centered around time travel is Prince of Persia: the sands of time
    Everything is time travel in that game. Story, movement and combat all implement it

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад

      Yeah I really liked that game when I was younger!

  • @PantheraLeo04
    @PantheraLeo04 9 месяцев назад +1

    You may have mentioned this and I just missed it, but if you're already recording velocity each frame, wouldn't recording position be redundant. If you have a starting position (the moment you begin rewinding time) and the velocity at each point in time, I'm pretty sure you should be able to just derive the position from those right? And the same would be true of the rotation and angular velocity.

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад

      Interesting, I guess that's true. It would probably be a good way to cut down on RAM usage, but if that's not an issue I think my One-Recorder-for-one-Component architecture is pretty clean and readable (I like that everything is separate). I don't know if calculating velocity ourselves yields results as good as reading them directly from the rb (although maybe deriving it from the difference in position is exactly what they do under the hood idk).
      But great suggestion!

    • @vonbaldric9767
      @vonbaldric9767 9 месяцев назад

      Unity physics probably aren't determistic enough to use dead-reckoning exclusively like that. If you scrubbed back far enough you would probably not arrive at the same positions that you originally passed through. You could do a hybrid system with 'key-frames' like how video compression works. You might be able to do the opposite and derive velocity from delta_position, though I can definitely imagine this resulting in velocity getting 'eaten' if you scrub back to just the wrong frame.

  • @TobyJWalter
    @TobyJWalter 9 месяцев назад

    I love your videos, keep up the amazing work! ♥

  • @KansasToYou
    @KansasToYou 9 месяцев назад

    Awesome video man. Great work

  • @edwardperkins1225
    @edwardperkins1225 9 месяцев назад

    I wonder if Nintendo will actual try suing people for time rewind mechanics in non-Nintendo games considering the US patient office was stupid enough to give them one for it in TotK. Maybe it only counts if you use a cursor to select what to rewind.

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад +1

      I don't know, they've been known to not sue people for these weird all-encompassing patents. It seems they do it more as a way to protect themselves in case someone tries to patent this and then comes after them. The "link is on an vehicle that moves therefore link moves" one is especially crazy.

  • @ladiesman2048
    @ladiesman2048 9 месяцев назад

    Iron Danger is a Unity game that has time rewind mechanic

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад +1

      Thanks I'll check it out!

    • @ladiesman2048
      @ladiesman2048 9 месяцев назад

      Looks like it's -90% at Steam this week so now is a perfect time to check it out :)

  • @Conman9310
    @Conman9310 9 месяцев назад

    couldnt you store the frame number when adding to the record and just use that number to find the most recently added one instead of a search

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад

      Do you mean storing the (frame, data) tuple as a struct in a list? because then iterating through the list would be O(n). Or you could still implement a binary search too, in a way both this and a dictionary store the frame id somewhere. Maybe the ultimate data structure is the KeyedCollection, it's the best of both worlds, I should use it more often.

  • @jokesterthemighty227
    @jokesterthemighty227 9 месяцев назад

    That's not how it's typically done, see with rollback network solution you already store a lot of previous frames so it's trivial to make them play backwards (in an offline setting)

  • @DarthBiomech
    @DarthBiomech 9 месяцев назад

    One question though, why is your solution was generic instead of a simple inheritance? at least in the video, I don't think any of the script examples seem to utilize .

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад +1

      Eh it appears you're right. I think it used to be that recorders would return their T components at some point but right now there isn't any use of T in the code. At least it's there for the future I guess

  • @jjrubes1880
    @jjrubes1880 9 месяцев назад

    How did you implement binary search? I can't think of how it would be possible if you've got gaps in the list without some indication of the nearest change

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад

      You search the list of *keys*, that's what you can split in half, en check if the median key is higher or below your desired frame, then you split again and again and again. The list of keys has continuous indices of course. I hope this makes sense

    • @jjrubes1880
      @jjrubes1880 9 месяцев назад

      That makes sense, but there isn't much point it being a hashmap any more as you could get the same look up efficiency with an array of (time, position) pairs. But, seeing as most of the time you're only looking 1 or 2 frames forwards or backwards it would make more sense just to store the index instead of recalculating it each frame, getting that sweet O(1).

  • @steluste
    @steluste 9 месяцев назад

    What's the name of your visual studio theme?

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад +1

      This is a Sublime Text theme called Mariana that I modified, I don't remember exactly what I did other than changing the background color.
      I do use Visual Studio when working but record the text snippets for the video in Sublime Text

  • @MarekNijaki
    @MarekNijaki 9 месяцев назад

    Great video

  • @oglothenerd
    @oglothenerd 8 месяцев назад +1

    Have you considered switching to Godot?

    • @uselessgamedev
      @uselessgamedev  8 месяцев назад +1

      I have considered learning Godot to add it to the technologies I know. It will take a while for me to reach full proficiency though

    • @oglothenerd
      @oglothenerd 8 месяцев назад +1

      @@uselessgamedev Does the Unity drama bother you at all?

  • @zaj007
    @zaj007 9 месяцев назад

    How's your indie game coming along?

    • @uselessgamedev
      @uselessgamedev  9 месяцев назад +2

      It's doing all right except we will soon run into the first obstacle of "we can't keep going until we get funding" and so far all our funding efforts have been in vain. So we will probably have to put the project on the back burner and take care of pre-production ourselves, and go back to seeking (less) funding when our prototype is more convincing

  • @0hellow797
    @0hellow797 9 месяцев назад

    How impossible would it be to implement a replay system that can fit on any game. I don’t want to think about it anyway lmao

  • @dantelaviero7782
    @dantelaviero7782 9 месяцев назад

    he coded a vegan robot

  • @reguret2976
    @reguret2976 9 месяцев назад

    boids!

  • @TheRojo387
    @TheRojo387 5 месяцев назад

    Sorry man, but Artoon beat you to it!

  • @awesomemike3857
    @awesomemike3857 9 месяцев назад

    Why do all of this when you can just set Time.timescale = -1 lol

  • @s1lkysl1m83
    @s1lkysl1m83 9 месяцев назад

    unity, LOL!