301 Days

A year of gamedev experiments.

Day 8 - Ghosts and Boars

| Comments

Stop seeing NPCs when they’re gone, and a little more variety…


“I see dead people.”

Ok, one last thing from our trouble list at the end of day five: Inactive NPCs will still show up.

This is a nice little problem we created for ourselves. The NPCLocation table shows the last known position of any NPC which has ever existed, and we’re not doing any filtering on the visualizer side to weed out the ones that aren’t there any more. First we’ll inject some code to make this problem more visible:

DragonsSpine/GameObjects/GameLiving/NPC/NPC.csGitLab
13
14
15
public class NPC : Character
{
    public int lastActiveRound = 0;
DragonsSpine/DAL/DBNPC.csGitLab
92
93
94
95
96
97
98
99
100
101
102
103
104
foreach (DataRow dr in dtNPCs.Rows)
{
    if (dr["name"].ToString() == "wolf") {
        NPC wolf = new NPC();
        wolf.worldNpcID = Convert.ToInt32(dr["NPCIndex"]);
        wolf.lastActiveRound = Convert.ToInt32(dr["active"]);
        wolf.Name = dr["name"].ToString();
        wolf.X = Convert.ToInt16(dr["xCord"]);
        wolf.Y = Convert.ToInt16(dr["yCord"]);
        wolf.Z = Convert.ToInt16(dr["zCord"]);
        npclist.Add(wolf);
    }
}
Managers/MapManager.csGitLab
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
void Update () {
    if (Time.time > nextDBUpdate) {
        print("Time to read from the DB! " + Time.time);
        nextDBUpdate = Time.time + dbUpdateDelta;
        activeGameRounds.Clear();
        UpdateCellContents();
        UpdateWolves();
    }
}

void UpdateWolves() {
List<NPC> wolves = DragonsSpine.DAL.DBNPC.GetWolves();
print("Wolves?  I found: " + wolves.Count);
var cellsContainingWolves = new Dictionary<Vector3,List<int>>();
foreach (NPC wolf in wolves) {
    if (!activeGameRounds.Contains(wolf.lastActiveRound)) activeGameRounds.Add(wolf.lastActiveRound);
Managers/GUIManager.csGitLab
27
28
29
30
31
32
33
34
35
void OnGUI(){
    GUI.Box (new Rect (0,0,100,50), "# of cells\n" + map_manager.num_cells);
    GUI.Box (new Rect (Screen.width - 100,0,100,50), "# of blocks\n" + map_manager.num_blocks);
    GUI.Box (new Rect (0,Screen.height - 50,100,50), "Z top\n" + map_manager.zPlanes[map_manager.currentZTopIdx]);
    StringBuilder builder = new StringBuilder();
    foreach (int gameRound in map_manager.activeGameRounds)
        builder.Append(gameRound).Append(" ");
    GUI.Box (new Rect (Screen.width - 100,Screen.height - 50,100,50), "Game Round\n" + builder.ToString());
}

There, we’ve given ourselves a way of keeping track of the last-active game rounds of all the NPCs (well, wolves) we’re rendering. Let’s see how that turns out:

As a first step, at least we can render the non-active ones as “ex” wolves. Let’s use a pair of Materials in the Resources directory and assign the appropriate one to the wolf’s Renderer:

Managers/MapManager.csGitLab
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
void UpdateWolves() {
    Material liveWolf = Resources.Load("Materials/Wolf", typeof(Material)) as Material;
    Material exWolf = Resources.Load("Materials/exWolf", typeof(Material)) as Material;
    List<NPC> wolves = DragonsSpine.DAL.DBNPC.GetWolves();
    print("Wolves?  I found: " + wolves.Count);
    var cellsContainingWolves = new Dictionary<Vector3,List<int>>();
    int maxGameRound = 0;
    foreach (NPC wolf in wolves) {
        if (wolf.lastActiveRound > maxGameRound) maxGameRound = wolf.lastActiveRound;
    }
    foreach (NPC wolf in wolves) {
        if (!activeGameRounds.Contains(wolf.lastActiveRound)) activeGameRounds.Add(wolf.lastActiveRound);
        Vector3 cell = new Vector3(wolf.X, wolf.Y, wolf.Z);
        wolfLocations[wolf.worldNpcID] = cell;
        if (!cellsContainingWolves.ContainsKey(cell))
            cellsContainingWolves[cell] = new List<int>();
        cellsContainingWolves[cell].Add(wolf.worldNpcID);
        Vector3 position = new Vector3((-1f) * wolf.X, (-1f) * wolf.Y, (0.1f * wolf.Z) + 0.2f);
        if (wolfTransforms.ContainsKey(wolf.worldNpcID)) {
            wolfTransforms[wolf.worldNpcID].position = position;
        } else {
            Transform tempts = Instantiate(wolfPrefab,
                                           position,
                                           Quaternion.identity) as Transform;
            wolfTransforms[wolf.worldNpcID] = tempts;
        }
        Renderer rend = wolfTransforms[wolf.worldNpcID].GetComponent<Renderer>();
        if (wolf.lastActiveRound >= maxGameRound-1) // one round leeway in case we catch the DB in mid-update
            rend.material = liveWolf;
        else
            rend.material = exWolf;
    }

This might work, but testing it proved…time-consuming. I restarted the server, and no wolves expired within the first couple hundred rounds. Let’s see if anything else is more fragile:


Detour: Boars and Crocodiles and Orcs, Oh My!

The “just wolves” thing worked for a bit, but now let’s truly handle other types of NPCs.

DragonsSpine/DAL/DBNPC.csGitLab
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
public static List<NPC> GetNpcs(string npcName)
{
    List<NPC> npclist = new List<NPC>();

    using (SqlConnection tempConnection = DataAccess.GetSQLConnection())
    using (SqlStoredProcedure sp = new SqlStoredProcedure("prApp_NPCLocation_by_MapID", tempConnection)) {
        sp.AddParameter("@facet", SqlDbType.Int, 4, ParameterDirection.Input, 0);
        sp.AddParameter("@land", SqlDbType.Int, 4, ParameterDirection.Input, 0);
        sp.AddParameter("@map", SqlDbType.Int, 4, ParameterDirection.Input, 0);
        DataTable dtNPCs = sp.ExecuteDataTable();
        foreach (DataRow dr in dtNPCs.Rows)
        {
            if (dr["name"].ToString() == npcName) {
                NPC npc = new NPC();
                npc.worldNpcID = Convert.ToInt32(dr["NPCIndex"]);
                npc.lastActiveRound = Convert.ToInt32(dr["active"]);
                npc.Name = dr["name"].ToString();
                npc.X = Convert.ToInt16(dr["xCord"]);
                npc.Y = Convert.ToInt16(dr["yCord"]);
                npc.Z = Convert.ToInt16(dr["zCord"]);
                npclist.Add(npc);
            }
        }
    }
    return npclist;
}
Managers/MapManager.csGitLab
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
void Update () {
    if (Time.time > nextDBUpdate) {
        print("Time to read from the DB! " + Time.time);
        nextDBUpdate = Time.time + dbUpdateDelta;
        activeGameRounds.Clear();
        UpdateCellContents();
        UpdateNpcs("boar");
        UpdateNpcs("crocodile");
        UpdateNpcs("orc");
        UpdateNpcs("wolf");
    }
}

void UpdateNpcs(string npcName) {
    Material liveNpc = Resources.Load("Materials/" + npcName, typeof(Material)) as Material;
    Material exNpc = Resources.Load("Materials/ex" + npcName, typeof(Material)) as Material;
    List<NPC> npcs = DragonsSpine.DAL.DBNPC.GetNpcs(npcName);
    print("I found " + npcs.Count + " NPCs called " + npcName);
    var cellsContainingNpcs = new Dictionary<Vector3,List<int>>();
    foreach (NPC npc in npcs) {
        if (npc.lastActiveRound > maxGameRound) maxGameRound = npc.lastActiveRound;
    }
    foreach (NPC npc in npcs) {
        if (!activeGameRounds.Contains(npc.lastActiveRound)) activeGameRounds.Add(npc.lastActiveRound);
        Vector3 cell = new Vector3(npc.X, npc.Y, npc.Z);
        npcLocations[npc.worldNpcID] = cell;
        if (!cellsContainingNpcs.ContainsKey(cell))
            cellsContainingNpcs[cell] = new List<int>();
        cellsContainingNpcs[cell].Add(npc.worldNpcID);
        Vector3 position = new Vector3((-1f) * npc.X, (-1f) * npc.Y, (0.1f * npc.Z) + 0.2f);
        if (npcTransforms.ContainsKey(npc.worldNpcID)) {
            npcTransforms[npc.worldNpcID].position = position;
        } else {
            Transform tempts = Instantiate(npcPrefab,
                                           position,
                                           Quaternion.identity) as Transform;
            npcTransforms[npc.worldNpcID] = tempts;
        }
        Renderer rend = npcTransforms[npc.worldNpcID].GetComponent<Renderer>();
        if (npc.lastActiveRound >= maxGameRound-1) // one round leeway in case we catch the DB in mid-update
            rend.material = liveNpc;
        else
            rend.material = exNpc;
    }
    // Any cohab?
    foreach (Vector3 cell in cellsContainingNpcs.Keys) {
        int population = cellsContainingNpcs[cell].Count;
        print("Cell " + cell + " has " + population + " of npc " + npcName);
        int dimension = (int) Math.Ceiling(Math.Sqrt(population));
        print("We can fit those in a " + dimension + "x" + dimension + " grid.");
        int row = 0, column = 0;
        foreach (int npcId in cellsContainingNpcs[cell]) {
            print(npcName + " " + row + "," + column + " is " + npcId);
            npcTransforms[npcId].localScale = new Vector3(1.0f/dimension, 1.0f/dimension, npcTransforms[npcId].localScale.z);
            Vector3 position = new Vector3((-1f) * cell.x, (-1f) * cell.y, (0.1f * cell.z) + 0.2f); // start at the center of the cell
            position += new Vector3(0.5f, 0.5f, 0f); // move to corner
            npcTransforms[npcId].position = position - new Vector3(column * (1.0f/dimension), row * (1.0f/dimension), 0f);
            npcTransforms[npcId].position -= new Vector3((1.0f/dimension)/2f, (1.0f/dimension)/2f, 0f);
            column += 1;
            if (column >= dimension) { column = 0; row += 1; }
        }
    }
}

Some public domain images and a lot of search-and-replace, and we now handle the NPCs generically and can track four of the common surface types. Different types in the same cell won’t look right, so that’s another TODO.

But do we get to prove out the corpse rendering? Yes we do.

Day 8 code - client


A brief note on debugging:

It may seem strange, the effort I went to in order to show that we were showing stale NPCs. I usually want to clearly show a bug occuring before implementing a fix, or else how do you know it’s fixed? Usually there’s some learning along the way, especially if it involves unfamiliar tech or a legacy codebase. Automated tests are another, often better, way of addressing this concern. We’ll get to that soon.

Comments