Search Unity

Looking for clean code practices - how to integrate loaded data into ongoing simulation

Discussion in 'Game Design' started by BIGTIMEMASTER, Jun 6, 2022.

  1. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    I apologize for the title, I can't find a simple way to phrase this question, though it is actually pretty simple.

    Suppose you have a racing game. When the player starts a race, you generate some data in preparation. Where does the race start and finish? What is the time limit and rewards? etc.

    Before the race is finished, the player might save and quit. In this case you are saving location, current time, and so on.

    Later, player can load the race again. It's no problem to take that saved data and plug it back in and end up where you left off, but doing so with clean, maintainable code is something I am trying to improve on.

    My initial approach was to just fire off the Race Preparation series of events again, BUT for select events I might ask, "is this a new race or are we loading data?"

    That works but it means that when it comes to debugging or making changes, I have a lot of hunting and pecking to do. I have caveats weaved into the code and that is both hard to manage and conceptualize. I think it stands in violation of the single responsibility rule.

    So my idea for improving this system is a two step process:
    1. I fire the original race preparation series of events again.
    2. In a separate block of logic, I override those variables which had been saved.

    I think this should break my code into more logical chunks. I have code for starting a race, and code for updating a race. The two are not woven together, so bug squashing should be simpler, as well as if I need to make any general edits.

    How might you handle such a problem? Does this sound sensible to you? Have you faced similar problems and learned any best practices that helped?

    The primary goal here is to make simple, clean code that is easy for a solo-developer to jump in and out of after being focused on something else like art for weeks.
     
  2. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    A graphic that illustrates the basic idea of separating the code
    upload_2022-6-6_16-9-35.png
     
  3. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,872
    Consider that your initial „starting“ state and events is nothing but a „load game“ with default values. Then it‘s simply a matter of implementing load events consistently with the occasional flag „race already in progress“ which for instance prevents the 3..2..1..GO! countdown and things like that.
     
    BIGTIMEMASTER likes this.
  4. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,619
    Yeah, this is pretty much what I do.

    My code has the concept of a "Persistent Object", which is an object who's state must be able to persist across multiple runs of the code. Each PersistentObject has a unique ID, and is responsible for two things:
    - At "load" time, initialise themselves with whatever state data is currently loaded. If there isn't any, use a default state.
    - At "save" time, pass all relevant state data over to to be stored on disk.

    To support this, there's a data storage system which just saves and loads the data. It starts up at the start of every game session and either loads some data in if a save file is specified, or just sits empty if one isn't.

    You could set this up so that you can just stick an attribute on data fields and it'll handle them for you. However, I don't mind just applying stored data "by hand" as there often needs to be a bit of code for the "default" cases anyway, sometimes you might want to sanity check the saved value before applying it, and I like every item having a nice, clear place where it's initialised rather than being done by background magic.

    It could look something vaguely like this (written for clarity, not necessarily good coding!):

    Code (csharp):
    1. class RaceCar : MonoBehaviour
    2. {
    3.   void Start()
    4.   {
    5.     persistentData = LoadPersistentData(thisObjectsID);
    6.  
    7.     if (persistentData.HasVector3("position"))
    8.     {
    9.       transform.position = persistentData.GetVector3("position");
    10.     }
    11.     else
    12.     {
    13.       transform.position = raceGrid.GetStartPosition(this);
    14.     }
    15.   }
    16.  
    17. }
     
    BIGTIMEMASTER likes this.
  5. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    @SteffenItterheim @angrypenguin

    I apologize guys but I did not see a notification that there was a response to this thread!

    Thanks for both your input.

    About using flags, that is what I initially did but I found that it made debug and editing to feel more cumbersome and time consuming than what seemed appropriate for what I was doing. It required too much memory on my brains behalf. Or time consuming because of needing to grab a flag and then ctrl+f to hunt around all the places it is used.

    I mean it worked but I felt like the code could be handled in a way... i dont know how to say it, but sometimes after some refactoring I end up with code where you look at it and everything is just like socks folded smartly in a drawer, lol. But this was like, some socks are in this drawer, some are on the floor, etc.

    @angrypenguin
    this is sounding similar to what I ended up doing. I am also using a persistent save object. When I save my race, I take all of that data that was generated to start it and put it into the save object.

    After I load, rather than run all the "create a race" code again, I figured out that all i have to do is update those data variables from the save object, and start the timer running again. It ended up being extremely simple to accomplish, although it was hard to plan because there is a lot of variables at play and I had to go through each one and ask, "does anything depend on this?".

    Luckily since I recently refactored my code to make better use of single responsibility principle in general, there wasn't any webs to untangle or cases where I have to handle data in a specific order, which is one of those things I was trying to improve.

    So, to summarize, when loading a race, I did not have an actual need to generate any data again - i only needed to update variables from the persistent save object, and also start the race timer running again. This meant that almost no new code needed to be written, other than just get and set some variables.

    I am not sure there is any universal principle to point to here, but I think the things that helped me improve the code was single responsibility principle most of all.
     
    angrypenguin likes this.