0
\$\begingroup\$

I am making a roguelike card game like dream quest, and I want to improve my dungeon/level save method.

Here's what my levels look like:

Screenshot of dungeon level

I checked dream quest saves but I don't like their method because it uses a simple text file for all the information. it's also really messy, repetitive and not extendable.

I used the format below for the first couple of months:

[
  [
    "Wall",
    "Wall",
    "Room",
    "Room",
    "Room",
    "Merchant",
    "Room",
    "Room",
    "Wall",
    "Wall",
  ],
  [
    "Wall",
    "Wall",
    ...
  ]
]

But at the time I did not have multiple enemies, merchants, items. I simply read the file by using an enum which matched the strings used in the save format (item, enemy, merchant etc.) then passed them into an array to create the board.

As I started to create different kinds of enemies, items, and merchants, my current game wouldn't support this. I don't want to create an enum like enemy1, enemy2, enemy3 ...

How can I improve the scalability & legibility of this save game format, so I don't have to keep adding new enum categories for each variety of content?

\$\endgroup\$
  • \$\begingroup\$ I just finished my better Save&Load implementation. Thank you all. I used the seed for the rooms, corridors and walls. I also write own class for every type like enemy, item, player etc. then save them in different files. \$\endgroup\$ – juba08 Jan 12 at 22:34
2
\$\begingroup\$

Your game save is already in JSON form, as a two dimensional array, but it is the most verbose possible way of representing the data. Some notes:

  • You are looking to make your format more concise, which means trying to make the data more readable / parseable by omitting data and restructuring. To do that you need to consider frequency of the values in your data.
  • If one value is by far more common than others (e.g. wall or empty space, most likely), then restructure the format to make that value implicit. I.e. "if not specified, the value for the cell is empty space."
  • Consider splitting out the data into parts that don't change (e.g. rooms and walls) and parts that do (e.g. items, enemies and merchants) into separate sections in your save format. This is especially important when you come need to store two values in the same cell (e.g. what type of room cell it is, plus the item present in that cell.)
  • 2D grid data is often easily stored as an image/bitmap (where the colour of the pixel maps to a particular cell value). However this is not particularly extensible or readable for storing dynamic data like where the enemies are stored. Nothing stops you from using both methods (image for walls/rooms, JSON for spawnable entities.)
  • If you have "sparse" entities, do not attempt to store them in a 2D array of values, instead store them as dictionaries which contain a co-ordinate plus any additional information about the item (e.g. { x: 2, y, 5, type: "sword"}
  • Even if you store entities in the 2D array, you can take advantage of JSON type flexibility and store either a simple cell value or a dictionary. E.g.:
[
  [
    "Wall",
    "Room",
    { type: "enemy", id: "enemy1", health: 50 },
    "Room",
    "Wall"
  ],
  [
    "Wall",
    "Room",
    "Room",
    { type: "item", id: "sword1", wear: 80 },
    "Wall"
  ]
]
  • Then at load time you check the cell, if it is a string interpret it as a simple cell type, and if it is a dictionary interpret it by looking at the contents of the dictionary.
  • Finally, as has been pointed out in other answers, don't store anything you can easily recreate; if using randomness to general levels, use a fixed seed and store only that seed, making sure that your level generation is predictable and repeatable. If your seed is used to generate initial positions and IDs for enemies, only do that process on the initial generation, on subsequent loads simply discard that information from the generation process and load the information on dynamic entities from your save state.
\$\endgroup\$
  • \$\begingroup\$ Thanks for brief reply I started making changes today but could not finish all. I will update when I made my changes. \$\endgroup\$ – juba08 Dec 30 '18 at 20:58
1
\$\begingroup\$

I would suggest using JSON.

Basically everything you create is either an object {}, an array [] or a data type (string, integer, float etc).

Here is an example of how you can implement this. Please note, I've used a # to represent a comment, so it is easier to understand what every symbol means, JSON does not support comments.

{ # an object representing a stage

    "name": "The name of my stage",

    # these are the tiles of your map. For the sake of this example, 0's represent a wall, and 1's represent empty (walkable) space.
    "map": [ 0, 1, 0, 0,
             0, 1, 1, 0,
             0, 0, 1, 0,
             0, 0, 1, 0],

    "enemies": [ # enemies is an array, so if you want to add a new enemy, just put a comma

        { # enemies[0]
            "positionX": 0,
            "positionY": 0,
            "positionZ": 0
        },

        { # enemies[1]
            "positionX": 0,
            "positionY": 0,
            "positionZ": 0
        }

    ]

    # here you can add more information about your stage

}

The last question is, how do you parse that the easy way? That's hard to answer without knowing what language you are working on, but there are plenty of JSON parsing libraries for most languages out there. The only reason I recommend it, is that its easy, simple, and scalable.

Depending on the languages, you can have a variable stage that saved the parsed data, and do something like stage.enemies[0] to access the first enemy, stage.enemies[1] for the second enemy etc. This way you can also iterate through the whole array, so as you keep adding new enemies, you won't have to edit your code.

\$\endgroup\$
  • \$\begingroup\$ I actually use JSON and parse it via StreamReader and fsJsonParser. Here is two sample that I use for more simple saves: pastebin.com/Bg1Sfg1b \$\endgroup\$ – juba08 Dec 29 '18 at 19:09
  • \$\begingroup\$ Thanks, I realized it might be better to use the similar pattern for dungeon levels too. I wanted to be store all data in the same enum array which has tile types because it was really easy to handle it but later it became harder to extend. Now I am thinking to change to separate enemy, item, merchant, player and room tile list. I will use List<Enemy>, List<Room> instead of TileType[][] \$\endgroup\$ – juba08 Dec 29 '18 at 19:23
  • \$\begingroup\$ @juba08 you seem to be on the right track, the code on pastebin that you mentioned does indeed show json code that scales, similar to my example. You can have an array representing the tiles of the map (as you already do), and another array representing the enemies and their location based on the previous map. That way you can easier control enemies and iterate through their array in case you are searching for something (for example which enemy can see you). \$\endgroup\$ – TomTsagk Dec 31 '18 at 14:08
0
\$\begingroup\$

Don't save the world you are procedurally generating.

Instead, save the random seed, then regenerate it.

That way, you only need to save the dynamic positions, such as the players and NPCs.

EDIT: Based on your comments, I suggest you read https://stats.stackexchange.com/q/354373/106928 and/or https://en.m.wikipedia.org/wiki/Random_seed to learn what is a random seed.

\$\endgroup\$
  • \$\begingroup\$ I did not understand how it will solve my problem. I am not also if it is possible to implement it my game because everything won't be random in the game. Are there any resources for Unity? \$\endgroup\$ – juba08 Dec 29 '18 at 19:04
  • \$\begingroup\$ Yes, Unity's random generator has a seed, which you have access to. I assume that the world is being randomly generated, since it is a rogue-like. \$\endgroup\$ – Evorlor Dec 29 '18 at 19:06
  • \$\begingroup\$ I checked some blog posts and videos for last hour but i could not find a good tutorial saving and loading using a seed. I started my roguelike part of the game with this tutorial and extended it: unity3d.com/learn/tutorials/projects/2d-roguelike-tutorial/… \$\endgroup\$ – juba08 Dec 29 '18 at 20:13
  • \$\begingroup\$ I use UnityEngine.Random.Range to determine how many objects will be in the board (Ex: min 4 max 6 enemies). I use a list of Free Tiles to if that tile is occupied or not. I use Random.Range (0, list.count) to find a free position then initiate that object there. At last i remove this tile from the list. Do you think random seed is practicable? \$\endgroup\$ – juba08 Dec 29 '18 at 20:14
  • \$\begingroup\$ @juba08 I updated my answer with some resources to help you understand what is a random seed, and why it applies here. \$\endgroup\$ – Evorlor Dec 29 '18 at 21:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.