Putting the 'role' back in role-playing games since 2002.
Donate to Codex
Good Old Games
  • Welcome to rpgcodex.net, a site dedicated to discussing computer based role-playing games in a free and open fashion. We're less strict than other forums, but please refer to the rules.

    "This message is awaiting moderator approval": All new users must pass through our moderation queue before they will be able to post normally. Until your account has "passed" your posts will only be visible to yourself (and moderators) until they are approved. Give us a week to get around to approving / deleting / ignoring your mundane opinion on crap before hassling us about it. Once you have passed the moderation period (think of it as a test), you will be able to post normally, just like all the other retards.

Vapourware Codexian Game Development Thread

Ninjerk

Arcane
Joined
Jul 10, 2013
Messages
14,323
Beginning work on a small Fallout/Arcanum inspired game. The idea is to create a small 2-3 hour adventure with tons of reactivity, choices & consequences, and replayability. I'm open to any design advice :)
Lays of Evenheart = deadgaem?
 

Tavernking

Don't believe his lies
Developer
Joined
Sep 1, 2017
Messages
1,217
Location
Australia
Beginning work on a small Fallout/Arcanum inspired game. The idea is to create a small 2-3 hour adventure with tons of reactivity, choices & consequences, and replayability. I'm open to any design advice :)
Lays of Evenheart = deadgaem?

It's on hold for now. I will probably scrap the current iteration and extend the new one from this upcoming fallout/arcanum inspired game, eventually.

I started working on (the current iteration of) Lays of Evenheart after a few months of learning how to code in 2019. It's a hugely ambitious and complicated game, so as you can imagine, the whole project became a nightmare to work with over the years. Now I'm a smarter programmer and very comfortable with my engine (Godot), the fallout/arcanum inspired game is being made very quickly. When it's completed it would be a much better place to continue the Lays of Evenheart project from.
 

Zanzoken

Arcane
Joined
Dec 16, 2014
Messages
3,574
Beginning work on a small Fallout/Arcanum inspired game. The idea is to create a small 2-3 hour adventure with tons of reactivity, choices & consequences, and replayability. I'm open to any design advice :)

The challenge with true C&C is that the player is only going to see a fraction of the content per playthrough. So if you want a 3-hour adventure with say 3 main paths, then you're probably creating 8 hours of content, i.e. almost triple the work. And then you have the additional effort of making sure each individual path works within the context of the others.

If you can do it properly though, it's a lot of fun. The best game I ever played in this regard is probably Age of Decadence. Thinking of Teron in particular, where each character background has a unique part of the story but they all fit together as a whole.
 

TheDeveloperDude

MagicScreen Games
Developer
Joined
Jan 9, 2012
Messages
408
Should I use auto notes in journal for everything?
For example in my game in the 2nd town you get a quest: revocer the Y relic.
And you find the relic in a dungeon after the 47th town. But who remembers where you have got this quest? I do not, that's sure.
So should I visit all the 47 town for give back the relic to the quest giver, or memorize it, or write it down on paper?
 

Bad Sector

Arcane
Patron
Joined
Mar 25, 2012
Messages
2,225
Insert Title Here RPG Wokedex Codex Year of the Donut Codex+ Now Streaming! Steve gets a Kidney but I don't even get a tag.
Automatic notes in journal are always helpful, especially if you do not have a traditional quest list (which, depending on the game, might become too "TODO-y"). Just make sure they contain the necessary information (e.g. who told you about a quest and where).

Do not rely on people writing/remembering stuff for things that the game itself could do (though allowing people to keep their own in-game notes, map pins, etc is always helpful and a nice thing to have, regardless of what the game does).
 

TheDeveloperDude

MagicScreen Games
Developer
Joined
Jan 9, 2012
Messages
408
Teh grafix is hard!
For example I want to write special good-looking green characters.
But I write it onto a yellow surface, and suddenly it seems a different green color, looking awful.

Btw, the demo of my MotUW game was a total failure. I have got exactly ZERO comment about that so far.
It can even be that nobody was able to run it.
 

Bad Sector

Arcane
Patron
Joined
Mar 25, 2012
Messages
2,225
Insert Title Here RPG Wokedex Codex Year of the Donut Codex+ Now Streaming! Steve gets a Kidney but I don't even get a tag.
Made a new version for Post Apocalyptic Petra. This is mainly about the performance improvements for the DOS and Windows versions for retro PCs and the Linux port of the editor. Here is a changelog (from the page above):
  • [Game] Benchmark mode (-bench parameter)
  • [Game] Add progressbar in loading screens
  • [Engine] Use a PAK file instead of data files in the data subdirectory. The -NOPAK parameter can be used to ignore the PAK file
  • [Engine] Most file I/O code was changed to use streams instead of raw files
  • [Engine] Audio fixes for Windows and DOS
  • [Engine] Optimization: treat box visibility requests beyond the camera as invisible
  • [Engine] Optimization: replace PVS with portals allowing for better occlusion culling
  • [Engine] Optimization: for the software rendering backends add occlusion tests against the recorded span data to avoid drawing meshes behind walls and following occluded portals
  • [DOS] Fix SB16 buffer initialization
  • [SDL] Optimize output scaling to actually not be awful
  • [DDraw] Optimization: do doublebuffering in the CPU as this can be faster for some slower cards
  • [D3D] Fix Direct3D use of fog so that S3 Virge works
  • [D3D] Add support for 8bit textures to save some VRAM
  • [S3D] Unfinished S3D backend for S3 Virge-based GPUs (mostly works except for clipping)
  • [Editor] Port editor to Linux using OpenAL for audio
  • [Editor] Allow hiding the triline objects used to mark in-game invisible stuff
  • [Editor] Add a bunch of development helping and debugging visualizations, wireframe mode, etc
  • [Editor] Fix various editor bugs
  • A few other minor things
 

Tavernking

Don't believe his lies
Developer
Joined
Sep 1, 2017
Messages
1,217
Location
Australia
Godot particles are very fun, these took 2 hours to make and most of that time was learning how to do it. I’m excited to see what I can make.

Top left - daze spell on a single target, note how it looks very dazey.
Middle - sleep spell on an area, note how it looks very dreamy and sleepy.
Bottom middle - particles and god rays emitting from a loot drop, inspired by Pillars of Eternity.

 

infidel

StarInfidel
Developer
Joined
May 6, 2019
Messages
494
Strap Yourselves In
I recall I was lamenting at some point how doing save games is hard. Since recently I did follow a new approach for me, I thought someone might find this useful. I don't know how you would do that in a language without reflection but if you have that, it removes a lot of pain from your shoulders. Basically, to save the game we feed the Game object into the recursive function that traverses the object tree, looking for any object of a class that extends SaveObject class and then partially serializes it into simple anonymous objects through reflection (a string map will probably do, too). Since a lot of objects might have some properties that are not necessary to restore the state, like related to graphics engine, for example, we create a class property like this:
Code:
static var _ignoredFields = [ 'texture', 'uiobject', ... ];
And then check each property name against it.

Save code:
Code:
// save object (recursively)
  function saveObject(name: String, o: Dynamic, depth: Int): Dynamic
    {
      if (depth > 20)
        throw 'Depth too high: ' + depth + ' ' + name;
      // basic type cases
      if (Std.isOfType(o, Int) ||
          Std.isOfType(o, Float) ||
          Std.isOfType(o, Bool) ||
          Std.isOfType(o, String))
        return o;
      if (Std.isOfType(o, Array))
        {
          var val = [];
          var tmp: Array<Dynamic> = o;
          for (el in tmp)
            val.push(saveObject(name + '[]', el, depth + 1));
          return val;
        }
      switch (Type.typeof(o)) {
        case TEnum(e):
          return {
            _classID: Type.getEnumName(e),
            _isEnum: true,
            val: '' + o,
          }
        default:
      }

      // ignored fields
      // kludge for ai/game object subclasses
      var ret: Dynamic = {};
      var cl = Type.getClass(o);
      var clname: String = null;
      if (cl != null)
        clname = untyped cl.__name__;
      ret._classID = clname;
      if (clname != null &&
          (StringTools.startsWith(clname, 'ai') ||
           StringTools.startsWith(clname, 'objects') ||
           StringTools.endsWith(clname, 'FSM')))
        {
          cl = Type.getSuperClass(cl);
          if (cl != null)
            {
              clname = untyped cl.__name__;
              // sub-subclass
              if (clname != 'ai.AI' &&
                  clname != 'objects.AreaObject')
                cl = Type.getSuperClass(cl);
            }
        }
      var ignoredFields: Array<String> =
        Reflect.field(cl, '_ignoredFields');

      // object, loop through fields
      for (f in Reflect.fields(o))
        {
          // circular links
          if (f == 'game')
            {
              ret._hasGame = true;
              continue;
            }
          if (f == 'ui')
            {
              ret._hasUI = true;
              continue;
            }
          var fobj: Dynamic = Reflect.field(o, f);
          if (ignoredFields != null && Lambda.has(ignoredFields, f))
            continue;

          // enums
          var fval: Dynamic = null;
          switch (Type.typeof(fobj)) {
            case TEnum(e):
              fval = {
                _classID: Type.getEnumName(e),
                _isEnum: true,
                val: '' + fobj,
              }
            default:
          }
          if (fval != null)
            1;
          else if (Std.isOfType(fobj, Int) ||
              Std.isOfType(fobj, Float) ||
              Std.isOfType(fobj, Bool) ||
              Std.isOfType(fobj, String))
            fval = fobj;
          else if (Std.isOfType(fobj, Array))
            {
              fval = [];
              var tmp: Array<Dynamic> = fobj;
              for (el in tmp)
                fval.push(saveObject(f + '[]', el, depth + 1));
            }
          else if (Std.isOfType(fobj, List))
            {
              fval = [];
              var tmp: List<Dynamic> = fobj;
              for (el in tmp)
                fval.push(saveObject(f + '[]', el, depth + 1));
            }
          else if (Std.isOfType(fobj, haxe.ds.IntMap))
            {
              fval = {};
              var tmp: Map<Int, Dynamic> = fobj;
              for (key => el in tmp)
                Reflect.setField(fval, '' + key,
                  saveObject(f + '[]', el, depth + 1));
            }
          else if (Std.isOfType(fobj, haxe.ds.StringMap))
            {
              fval = {};
              var tmp: Map<String, Dynamic> = fobj;
              for (key => el in tmp)
                Reflect.setField(fval, key,
                  saveObject(f + '[]', el, depth + 1));
            }
          // serializable objects
          else if (Std.isOfType(fobj, _SaveObject))
            fval = saveObject(f, fobj, depth + 1);
          else continue;
          Reflect.setField(ret, f, fval);
        }
      return ret;
    }
Pretty readable in the end. The result is that running that on the Game object will give us a very clean anonymous object that we can feed into the JSON or another serializer. Most of the work is done automatically but we have to keep in mind handling the special properties that might need some partial state saved (like current animation state or sounds played or texture IDs).

I think that took me 3 or 4 evenings, and a lot of that was carefully going through the code and writing down what had to be dealt with in special way on load. Half of the job done, right? Well, not really... Now we need to load the game back. That has two immediate main parts: firstly, some of the important objects (including the instance of Game itself) are already created and the savegame state has to be merged into them property by property, and secondly, objects like AI and items have to be instanced anew and then savegame state glued over. And that's not all, after loading all the "game" state, the graphics, UI and sound state has to be recreated from the game state as it was. So we do this in the following order:
  • Init a new game
  • Load the savegame recursively merging "source" savegame object into "destination" Game object
  • Call the post-load hooks to fix the remaining things into the workable state
Load code:
Code:
// load source object into destination object recursively
  function loadObject(src: Dynamic, dst: Dynamic, name: String, depth: Int)
    {
      for (f in Reflect.fields(src))
        {
          // ignore class ID marker
          if (f == '_classID')
            continue;
          var srcval: Dynamic = Reflect.field(src, f);
          var dstval = Reflect.field(dst, f);
          var isEnum: Bool = untyped srcval._isEnum;
          var classID: String = srcval._classID;
          // enum cases
          switch (Type.typeof(dstval)) {
            case TEnum(e):
              Reflect.setField(dst, f, initEnum(name, srcval, depth + 1));
              continue;
            default:
          }
          if (isEnum)
            {
              Reflect.setField(dst, f, initEnum(name, srcval, depth + 1));
              continue;
            }

          if (Std.isOfType(srcval, Int) ||
              Std.isOfType(srcval, Float) ||
              Std.isOfType(srcval, Bool) ||
              Std.isOfType(srcval, String))
            Reflect.setField(dst, f, srcval);

          else if (
              Std.isOfType(srcval, Array) ||
              Std.isOfType(dstval, Array) ||
              Std.isOfType(dstval, List))
            {
              var dsttmp = new Array<Dynamic>();
              var srctmp: Array<Dynamic> = untyped srcval;
              for (el in srctmp)
                {
                  if (Std.isOfType(el, Int) ||
                      Std.isOfType(el, Float) ||
                      Std.isOfType(el, Bool) ||
                      Std.isOfType(el, String))
                    dsttmp.push(el);
                  // NOTE: we only use Array<Array<Int>> currently
                  else if (Std.isOfType(el, Array))
                    {
                      // assume int atm
                      dsttmp.push(el);
                      continue;
                    }
                  var elClassID: String = untyped el._classID;
                  var isEnum: Bool = untyped el._isEnum;
                  if (elClassID == null)
                    dsttmp.push(el);
                  else if (isEnum)
                    dsttmp.push(initEnum(name, el, depth + 1));
                  else
                    {
                      var dstel = initObject(name + '.' + f + '[][]', el, depth);
                      dsttmp.push(dstel);
                    }
                }
              if (Std.isOfType(dstval, List))
                Reflect.setField(dst, f, Lambda.list(dsttmp));
              else Reflect.setField(dst, f, dsttmp);
            }

          else if (Std.isOfType(dstval, haxe.ds.IntMap))
            {
              var dsttmp = new Map<Int, Dynamic>();
              for (ff in Reflect.fields(srcval))
                {
                  var el = Reflect.field(srcval, ff);
                  var key = Std.parseInt(ff);
                  if (Std.isOfType(el, Int) ||
                      Std.isOfType(el, Float) ||
                      Std.isOfType(el, Bool) ||
                      Std.isOfType(el, String))
                    dsttmp.set(key, el);
                  var elClassID: String = untyped el._classID;
                  if (elClassID == null)
                    dsttmp.set(key, el);
                  else
                    {
                      var dstel = initObject(name + '[' + ff + ']', el, depth);
                      dsttmp.set(key, dstel);
                    }
                }
              Reflect.setField(dst, f, dsttmp);
            }

          else if (Std.isOfType(dstval, haxe.ds.StringMap))
            {
              var dsttmp = new Map<String, Dynamic>();
              for (ff in Reflect.fields(srcval))
                {
                  var el = Reflect.field(srcval, ff);
                  if (Std.isOfType(el, Int) ||
                      Std.isOfType(el, Float) ||
                      Std.isOfType(el, Bool) ||
                      Std.isOfType(el, String))
                    dsttmp.set(ff, el);
                  var elClassID: String = untyped el._classID;
                  if (elClassID == null)
                    dsttmp.set(ff, el);
                  else
                    {
                      var dstel = initObject(name + '[' + ff + ']', el, depth);
                      dsttmp.set(ff, dstel);
                    }
                }
              Reflect.setField(dst, f, dsttmp);
            }
          else if (Std.isOfType(dstval, _SaveObject))
            {
              loadObject(srcval, dstval, f, depth + 1);
              Reflect.setField(dst, f, dstval);
            }
          else if (dstval == null)
            {
              dstval = initObject(name + '.' + f, srcval, depth);
              Reflect.setField(dst, f, dstval);
            }
          else trace(name + '.' + f + ' type is unsupported (' +
            classID + ').');
        }

      // common fields
      var hasUI: Bool = untyped src._hasUI;
      if (hasUI == null)
        hasUI = false;
      if (hasUI)
        dst.ui = this.ui;
      var hasGame: Bool = untyped src._hasGame;
      if (hasGame == null)
        hasGame = false;
      if (hasGame)
        dst.game = this;
    }

// will init enum from src data { classID, isEnum, val }
  function initEnum(name: String, src: Dynamic, depth: Int): Dynamic
    {
      var classID: String = untyped src._classID;
      var ee = Type.resolveEnum(classID);
      if (ee == null)
        throw "No such enum: " + classID;
      return Type.createEnum(ee, untyped src.val);
    }

// will create a new class instance and populate it with data from save object
  function initObject(name: String, src: Dynamic, depth: Int): Dynamic
    {
      var isEnum: Bool = untyped src._isEnum;
      if (isEnum)
        return initEnum(name, src, depth);

      // common fields
      var hasUI: Bool = untyped src._hasUI;
      if (hasUI == null)
        hasUI = false;
      var hasGame: Bool = untyped src._hasGame;
      if (hasGame == null)
        hasGame = false;

      var srcClassID: String = untyped src._classID;
      var srcClass = Type.resolveClass(srcClassID);
      if (srcClass == null)
        throw 'Could not resolve class ' + srcClassID + ' src:' + src;
      var dst = Type.createEmptyInstance(srcClass);
      if (hasGame)
        dst.game = this;
      if (hasUI)
        dst.ui = this.ui;
      if (dst.init != null)
        dst.init();
      else trace('no init for ' + name);
      loadObject(src, dst, name, depth + 1);
      if (dst.initPost != null)
        dst.initPost(true);
      return dst;
    }

Enums are a little weird in Haxe, so I have to use the { classID, isEnum, val } object to get all the information I need to restore. initObject() method will create a new object from the classID, call init() for it if it's there, then merge savegame data on top and then call initPost() if it exists. Why so? That is due to the third problem - we have to have some things created for the engine to run on top of game objects, and this should work in two cases - when we create the object during running the game and when we load the object from the savegame. init() would be unnecessary if I could create the new object through the normal constructor (I can't, unfortunately). Everything that needs to be upfront, goes into the init(), everything that is later, goes into the initPost(). In my case I add both of these to the end of each constructor. Step three, "call the post-load hooks to fix the remaining things into the workable state" is a separate step from that, btw. Since there will be some leftovers that you cannot setup until after you've loaded the game.

Grinding through all the cases and bugs took me 2-3 weeks (and there were some bugreports after that, too, of course). At some point I was half-jokingly thinking about releasing the update with the ability to save but not to load. "Hey, you asked for savegames? Well, here they are!" :D The last problem that seemingly appeared out of nowhere, was due to my favorite method of working with dynamic languages. Imagine that you have a QuestObject that you have to do some custom interaction with somewhere in a single quest. There's literally one in the game (just like that thing in Soulash which I've just read the review about). Well, what could be easier?:

Code:
var o = new QuestObject(...);
o.onAction = function (...) {
  // do stuff on action, like activating a macguffin of doom or setting a quest flag, etc
};
Well, how do you serialize it? Oh shit, you can't serialize object methods (maybe you can but I can't). That's a pain, since this allows all the code to be in the same place and is very handy to work with. In the end I've had to make a string map of custom methods in the quest-related part of the code and then save the string ID of the method I want to use on that event object. Maybe a little clunky but nevermind. What's more annoying is that now I know that I can't use my old method going forward in a lot of cases - the savegames need to work.

I haven't talked about versioning yet since this is the first version. But thinking it through, there are the usual migration problems. You have to support some backwards compatibility. Do not remove anything so that savegames would not reference dead static data, and if you do, say that savegames earlier from version X will not be loaded. Try to cement the object structure early on, so that later on you would not need to change it. If you do, you're out of luck - a specialized version X to X+1 migration method will have to be made and tested to work. For example, you could store the inventory objects as a list of item IDs. So when you decide to add amount, you're out of luck - you'll either have to abandon old savegames support or make a method that will load an old array of strings into the new array of objects.
 

Twiglard

Poland Stronk
Patron
Staff Member
Joined
Aug 6, 2014
Messages
7,238
Location
Poland
Strap Yourselves In Codex Year of the Donut
Basically, to save the game we feed the Game object into the recursive function that traverses the object tree, looking for any object of a class that extends SaveObject class and then partially serializes it into simple anonymous objects through reflection (a string map will probably do, too)

Watch for cycles and other ways of having the same datum referenced multiple times. What you want to do is store the objects indexed by consecutive unsigned integers, then reference members of the subtree by their id's.

0: { "this": "is a root object", "a": <1>, "b": <1>, "c": <2>
1: array of [<3>, <0>, <4>]
2: {"foo": "bar"}
3, 4: etc...

Then you can do a === b still returning true after deserialization, and the whole thing not blowing up when you start including cycles.

For simplicity's sake, strings presented as literals but normally they'd be included as indexed elements as well.
 

Bad Sector

Arcane
Patron
Joined
Mar 25, 2012
Messages
2,225
Insert Title Here RPG Wokedex Codex Year of the Donut Codex+ Now Streaming! Steve gets a Kidney but I don't even get a tag.
Yeah, basically using object serialization to do saving is how many engines do it (often in a special "saving" mode to only serialize fields that need to be saved) - i think even Unreal 1 has this. For languages that do not have native RTTI or reflection support (or at least their RTTI isn't detailed enough) like C and C++, this is usually done with either macros (in C++ that'd be macros that auto-create objects during initialization, in plain C it'd be a bit more convoluted with x-macros) that describe the objects to serialize or some extra tool that runs at build time to parse the header files and generate code that registers the objects. Witcher 2 and 3 used the former approach (macros), Unreal Engine 4 (and most likely 5) use the latter.

Both Post Apocalyptic Petra's engine and Little Immersive Engine use Free Pascal's RTTI, which while not as powerful as a full blown reflection system, it does provide enough metadata to implement serialization. None of these have savegame support though (for Petra because when i made the game i ran out of time to implement savegames and the game can be completed in a single sitting anyway and for Little Immersive Engine because i just haven't worked on that yet but it'll be something i'll implement when i start working on gameplay support functionality) but the serialized data is simply a series of "set this property to that value" which would be enough (entities have GUIDs so they can be matched even with partial data). I don't remember if in Petra i store any type data, but in Little Immersive Engine i store both type data and "skipping" data and there is also a callback on the base serializable class (all objects that can be serialized descend from that class) so that when an object is deserialized it can be notified when an unknown property or type is loaded so that any conversions can be handled. The "skipping" data is used to simply skip over unknown properties/values whenever something cannot be handled so that the rest of the object can still be loaded (you don't want to be unable to load a world file because you, e.g. deleted a property of some random element in an entity).

Aside from savegames, having all game objects be serializable has other benefits too - for example in both Petra and Little Immersive Engine, the editor is working with the exact same objects that the game works with - you are basically manipulating "live" objects and just saving their state on disk when saving a map file or whatever other asset. In Petra, when working in the editor you can play the game inside the editor - this is done by serializing the entire game object (which contains all the game state, current map, etc) in a memory buffer, deserializing a new game object from the memory buffer, swapping the editor's game object with the newly created object and running the game on that. Then once the game exits (or the play mode is ended) the two objects are swapped again and the newly created object is destroyed. In addition to avoiding changes in the game affect the edited map, the editor can also be used with the objects inside the game when it is running (e.g. for inspection).

In Little Immersive Engine all asset references are also tracked (Petra uses plain strings to refer to assets since they're very simple). This required the serialization code to become aware of "external object references", i.e. objects that aren't to be serialized whenever the object that references them is serialized but are provided by some other means (this is done by using a GUID per such object). Assets are such external objects and whenever an asset (e.g. a mesh) is serialized and has a property that refers to another asset (e.g. a material), then instead of also serializing that object, a GUID that represents that object is serialized. Later when the asset is deserialized, a registry of "external object providers" is scanned for providers that can provide objects based on their GUID - one such provider is the asset manager that at startup scans all asset files for their GUIDs (the asset file format contains each asset's GUID at the first few bytes of the file so there is no need for full deserialization) and in the editor keeps track of all new assets. So when, e.g. a mesh is loaded, the material properties are not stored with the mesh (since they can be shared with other meshes) but instead the serialization system asks the asset manager for them (assets are also reference counted so they are unloaded automatically whenever they are not needed but this is specific to assets and not something that is part of the serialization system). In fact the engine doesn't even know about asset filenames, paths, etc, these are only used in the editor - the engine only knows about asset GUIDs (something that allows renaming or moving asset files around without breaking existing references).

In addition to allowing for practically automatic asset handling and saving and loading any sort of object that can be represented with Free Pascal's RTTI, it also allows for keeping track of which assets depend on which other assets - for example this world asset depends on a mesh and a material (used by brushes):

m0ovv50.png


(these are the direct dependencies meaning that the asset itself relies on those, however all dependencies - including those of the assets that the world asset depends on - can be found in the "Asset Dependencies" tab and the reverse, i.e the assets that depend on the asset being inspected, can be found in the "Dependent Assets" tab)

Keeping track of asset dependencies, aside from the obvious (i.e. not wasting space when distributing the game with unused assets - some versions of Post Apocalyptic Petra, which doesn't do asset tracking, did exactly that), has a number of other potential benefits - like, e.g. storing assets in package files with dependencies clustered together, which can be faster to load in HDDs (though TBH i don't know how relevant that'd be nowadays).
 

infidel

StarInfidel
Developer
Joined
May 6, 2019
Messages
494
Strap Yourselves In
Basically, to save the game we feed the Game object into the recursive function that traverses the object tree, looking for any object of a class that extends SaveObject class and then partially serializes it into simple anonymous objects through reflection (a string map will probably do, too)

Watch for cycles and other ways of having the same datum referenced multiple times. What you want to do is store the objects indexed by consecutive unsigned integers, then reference members of the subtree by their id's.

0: { "this": "is a root object", "a": <1>, "b": <1>, "c": <2>
1: array of [<3>, <0>, <4>]
2: {"foo": "bar"}
3, 4: etc...

Then you can do a === b still returning true after deserialization, and the whole thing not blowing up when you start including cycles.

For simplicity's sake, strings presented as literals but normally they'd be included as indexed elements as well.

In most cases I've solved this by replacing the link property with dynamic getter thing and numeric or string ID near it that is saved. In some cases I add the circular links to _ignoredFields and repopulate them after the loading since all the static data is already in the special ingame structures and referenced from there.
 

The Avatar

Pseudodragon Studios
Developer
Joined
Jan 15, 2016
Messages
336
Location
The United States of America
Here is a screenshot of the game data editor I made for Archquest:
1655498007520.png


All of the ancestries(races), classes, feats, spells, monsters, and items are edited here. Initially, things like feats and spells were hard coded- so for example I would have a class called "Fireball" that inherited from some base Spell class and contained all of the logic for that spell. I have since moved to a system where game logic is constructed from data defined in this editor, and then interpreted in-game. Much easier this way to maintain the hundreds of feats and spells to make.

Sadly, I don't think there is an easy to make this available to players to edit the game, because Unity. If I had to start over, I'd probably not use Unity- but it's too late now.

Weapons and armor are slightly different. Instead of having each weapon/armor store all of its data for each and every item, they instead store a reference to its entry in the weapon/armor data table, which could then be modified in any way if needed. This will make save games significantly lighter and hopefully prevent save game bloat and load time bloat seen in some other games.
1655498715561.png
 

MF

The Boar Studio
Patron
Developer
Joined
Dec 8, 2002
Messages
905
Location
Amsterdam
Sadly, I don't think there is an easy to make this available to players to edit the game, because Unity. If I had to start over, I'd probably not use Unity- but it's too late now.
If these are property containers for serializable objects, you can just share the c# files for these editor extensions somewhere on github or whatever. Anyone can download the free version of Unity and generate serializable objects this way. If you pull them from /Resources in your game, people can add them directly from the file system and mod the game this way.
 

TheDeveloperDude

MagicScreen Games
Developer
Joined
Jan 9, 2012
Messages
408
After the huge success of Steel Breeze Empire and Masters of the Unknown Worlds I have decided to make a Football Manager clone game.
Here is a pre-gameplay video.
 

Tavernking

Don't believe his lies
Developer
Joined
Sep 1, 2017
Messages
1,217
Location
Australia
After the huge success of Steel Breeze Empire and Masters of the Unknown Worlds I have decided to make a Football Manager clone game.
Here is a pre-gameplay video.

Here's an idea: make it so that the players are 6 year olds, and you are a Dad managing and coaching the team. Then you don't have fix the bad AI, and the bad AI contributes to the charm. It also gives you a unique selling point, so you're not like all the other football manager clones.
 
Joined
Jan 14, 2018
Messages
50,754
Codex Year of the Donut
Sadly, I don't think there is an easy to make this available to players to edit the game, because Unity. If I had to start over, I'd probably not use Unity- but it's too late now.
If these are property containers for serializable objects, you can just share the c# files for these editor extensions somewhere on github or whatever. Anyone can download the free version of Unity and generate serializable objects this way. If you pull them from /Resources in your game, people can add them directly from the file system and mod the game this way.
It's one of Unity's very few massive perks over Unreal tbh. UE4+ games are a mess to mod content-wise or script-wise.
Conan Exiles deals with this by distributing a very specific build of UE4 that they make the game with. The problem is: the editor is fucking massive. It's something like a 200GiB download.
 
Joined
Jan 14, 2018
Messages
50,754
Codex Year of the Donut
In my free time I like to watch a couple GDC videos and I can't help but realize the newer ones are nearly all complete trash. Without fail, they seem to be about throwing away how video games were made and replacing them with new, unproven concepts for... reasons. There typically isn't any good reason or evidence given for doing this beyond "new thing good, old thing bad"
e.g.,
 

Tavernking

Don't believe his lies
Developer
Joined
Sep 1, 2017
Messages
1,217
Location
Australia
In my free time I like to watch a couple GDC videos and I can't help but realize the newer ones are nearly all complete trash. Without fail, they seem to be about throwing away how video games were made and replacing them with new, unproven concepts for... reasons. There typically isn't any good reason or evidence given for doing this beyond "new thing good, old thing bad"
e.g.,


Product is popular, product is profitable, but it doesn't have many new things to give, so it starts making filler trash. Same thing for GDC talks as for books, games, TV shows, etc.
 

As an Amazon Associate, rpgcodex.net earns from qualifying purchases.
Back
Top Bottom