Skip to main content
  1. Posts/

Day 1 - My pencils are sharp enough

OldDays ste-reez-muvi csharp Unity

( with apologies to zefrank )

Tonight I’ll be trying to optimize the loading of the map so I don’t wind up with over ten thousand rendered objects for just the terrain with no characters or anything. I’ve already sketched out a couple of things to try, and made an initial attempt that gave me some confusing results.

But first some context:


What?
#

Inigo begins to explain
Inigo begins to explain

In the mid-1980s there was a commercially successful, text-graphics MMO hosted on CompuServe.
A few years back I was amazed to stumble across an open-source emulator, and cloned the repository in anticipation of having time to contribute. It’s since gone back to closed-source, so that’s the latest code I have. It’s a frankly awesome piece of work, and a great starting point for me to do some technology learning and experimentation while also satisfying my nostalgia.

The first thing I’m doing is making a visualizer for a running server, giving a bird’s eye view of the map. This gives me an excuse to play around more in Unity, and since the server code was already in C#, it is fairly easy to leverage.


Let’s do some stuff
#

Dr. Leo Marvin suggests Baby Steps
Dr. Leo Marvin suggests Baby Steps

So I put together a Unity project with a cube prefab and some hand-made ugly textures, brought in the DS code (commenting out the parts that expect a live database), and threw together something to render the map (Map and Cell are classes from the DS code):

ourMap = new Map();
string fileName = "Assets/IoKesmai.txt";
if (ourMap.LoadMap(fileName, 0, 0, 0)) {
  print ("Loaded map. " + ourMap.cells.Count + " cells loaded.");
  List<string> list = new List<string>(ourMap.cells.Keys);
  cell_transforms = new Dictionary<Vector3, Transform>();
  foreach (string k in list)
  {
    Vector3 map_position = new Vector3(ourMap.cells[k].X, ourMap.cells[k].Y, ourMap.cells[k].Z);
    Material cell_material = null;
    // Find the material index for this graphic
    for(int i = 0; i < cellMatString.Length; i++) {
      if (ourMap.cells[k].DisplayGraphic == cellMatString[i]) {
        cell_material = cellMat[i];
      }
    }
    Vector3 position = new Vector3((-1f) * ourMap.cells[k].X, (-1f) * ourMap.cells[k].Y, ourMap.cells[k].Z);
    Transform tempts = Instantiate(prefab,
                                  position,
                                  Quaternion.identity) as Transform;
    cell_transforms[map_position] = tempts;
    Renderer rend = tempts.GetComponent<Renderer>();
    rend.enabled = false;
    if (cell_material != null) {
      rend.enabled = true;
      rend.material = cell_material;
    } else {
      print("No match for cell display string \"" + ourMap.cells[k].DisplayGraphic + "\"");
      rend.material = cellMat[0];
      rend.enabled = true;
    }
    num_cells++;
  }
  num_blocks = cell_transforms.Count;
  print("Instantiated " + num_cells + " cells in " + cell_transforms.Count + " transforms.");
} else {
  print ("Failed to load map.");
}

It’s pretty awkward, especially the way we have to maintain parallel arrays to match up display strings to Materials (that’s just so I could easily mess w/it in the Unity editor). But it does get us this:

Map overview
Map overview

From this angle you can see the town of Kesmai and several of the dungeon levels underneath.

Map close-up
Map close-up

As you can see, I couldn’t make up my mind about the textures: retro green-on-black ASCII or something more representative?


First naive optimization
#

blocks for 11945 cells
blocks for 11945 cells

Having 11945 individual cubes just for the map seems a little wasteful. As a first pass, maybe we can just have a double-wide block if we encounter the same cell string twice in a row:

bool need_to_instantiate = true;
Vector3 prev_block_position = new Vector3(ourMap.cells[k].X - 1, ourMap.cells[k].Y, ourMap.cells[k].Z);
if (cell_transforms.ContainsKey(prev_block_position)) {
  Transform prev_block_transform = cell_transforms[prev_block_position];
  Renderer prev_block_rend = prev_block_transform.GetComponent<Renderer>();
  if (prev_block_rend.sharedMaterial.name == cell_material.name) {
    prev_block_transform.position += new Vector3(-0.5f, 0f, 0f);
    prev_block_transform.localScale += new Vector3(1f, 0f, 0f);
    need_to_instantiate = false;
  }
}
if (need_to_instantiate) { /* do stuff */ }

Seems simple enough: If there’s a block already instantiated to our left (x - 1), check its material. If it’s the same as I’m about to use, just shove that one over by a half unit and make it a unit wider (and don’t bother to instantiate the current one). What could go wrong?

7991 blocks for 11945 cells
7991 blocks for 11945 cells
8003 blocks for 11945 cells
8003 blocks for 11945 cells
7990 blocks for 11945 cells
7990 blocks for 11945 cells

Three runs in quick succession actually give us three different counts for what we’ve instantiated. Maybe we’re in a race with Unity, checking out the previous block’s renderer before it’s quite ready.

Let’s figure out what’s going on…

Time passes&hellip;
Time passes…

Well, that was a timesink. Not an issue with Unity’s renderers, not a C# collection oddity, it turns out there is some randomness in the original map-loading code that affects the cell graphic string, e.g.:

int randomTree = Rules.RollD(1, 300);
int randomBerries = Rules.RollD(1, 400);

if (randomTree >= 0 && randomTree < 41)
{
    cell.CellGraphic = " @";
}
else if (randomTree > 40 && randomTree < 81)
{
    cell.CellGraphic = "@ ";
}
else
{
    cell.CellGraphic = "@@";
}

Randomness is all well and good, but I can’t have that if I’m to be comparing optimizations. I don’t want to start messing with that code yet, so luckily the random number generator already had an alternate constructor that takes a seed (the default one uses the clock):

public RandMT()
{
    init_genrand((uint)DateTime.Now.Millisecond);
}

public RandMT(int seed)
{
    init_genrand((uint)seed);
}

Setting the seed to 1, I get the consistent results I was expecting.

8007 blocks for 11945 cells
8007 blocks for 11945 cells

Even this simple attempt gets us almost a one-third savings in actual objects to instantiate. Tomorrow we’ll try some real ideas.

Day 1 code