301 Days (redux)

Another metric leap year of gamedev experiments and such

Sep 17, 2015 - OldDays ste-reez-muvi

Day 16 - Cry "Havoc!", and let slip the centaurs of war.

Time for a little payoff, now that we have the violent info at hand.


Interpersonal Conflict

You wouldn’t like me when I’m angry.

Ok, yesterday was a bit of a letdown; once we were done, the visuals just looked exactly the same as they had before. But now we can prove that Knowledge is Power and show more of the action. Damage indicators seems like a good start.

In order to test out results quickly, we’ll need to foment a little discord on this mostly-peaceful map. Let’s talk to the SQL server (edited a bit for conciseness):


> SELECT npcID, name FROM NPC WHERE name='Sven';
npcID       name
----------- --------------------------------------------------------------------------------
20000       Sven

(1 row(s) affected)

> SELECT spawnX, spawnY, spawnZ from SpawnZone WHERE npcID=20000;
spawnX      spawnY      spawnZ
----------- ----------- -----------
41          5           0

(1 row(s) affected)

> SELECT zoneID, spawnX, spawnY, spawnTimer, notes from SpawnZone WHERE spawnMap=0 and maxAllowedInZone>1 and spawnZ=0;

zoneID      spawnX      spawnY      spawnTimer  notes
----------- ----------- ----------- ----------- --------------------------------------------------------------------------------
16          21          9           160         Boars, Wolves, Orcs West Kesmai
17          66          16          160         Boars, Wolves, Orcs
18          84          24          160         East Kesmai Wilds
20          117         24          160         Far East Kesmai Wilds
21          116         9           160         Centaurs

(5 row(s) affected)

> SELECT * from SpawnZone WHERE zoneID=21;

zoneID      notes                                                                            enabled npcID       spawnTimer  maxAllowedInZone spawnMessage                                                                     minZone     maxZone     npcList                                                                          spawnLand   spawnMap    spawnX      spawnY      spawnZ      spawnRadius spawnZRange
----------- -------------------------------------------------------------------------------- ------- ----------- ----------- ---------------- -------------------------------------------------------------------------------- ----------- ----------- -------------------------------------------------------------------------------- ----------- ----------- ----------- ----------- ----------- ----------- --------------------------------------------------------------------------------
21          Centaurs                                                                         1       16          160         10               NULL                                                                             0           0           NULL                                                                             0           0           116         9           0           4           NULL

(1 row(s) affected)

> INSERT INTO SpawnZone (notes, enabled, npcID, spawnTimer, maxAllowedInZone, spawnMessage, minZone, maxZone, npcList, spawnLand, spawnMap, spawnX, spawnY, spawnZ, spawnRadius, spawnZRange)
>   VALUES ('temp centaurs in town', 1, 16, 15, 10, NULL, 0, 0, NULL, 0, 0, 41, 11, 0, 4, NULL);
   
(1 row(s) affected)

We found where Sven (the guy behind the counter in the temple) spawns, and duplicated a centaur spawning zone (with faster respawning) just a bit south of that. That should make the town more exciting. Let’s see:

Citizens destroying centaurs that now spawn in town. Citizens destroying centaurs that now spawn in town.

Certainly more exciting, but we just see the centaurs suddenly becoming corpses. We need to at least see them being damaged:

Just a red partially-transparent block. Just a red partially-transparent block.

We’ll drop this half-transparent red block in front of damaged NPCs, and scale it based on the damage.

Assets/NpcScript.cs in Update() GitLab
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
        // display damage
        health = Mathf.Clamp((float) hits / (float) hitsFull, 0f, 1f);
        if (health < 1f && !presumedDead) {
            if (myDamage == null) {
                myDamage = Instantiate(damagePrefab, 
                                       Vector3.zero,
                                       Quaternion.identity) as Transform;
                myDamage.parent = this.transform;
                myDamage.localPosition = new Vector3(0f, 0f, 0.1f);
                myDamageRend = myDamage.GetComponent<Renderer>();
            }
            myDamage.localScale = new Vector3(1f, 1f - health, 1f);
            myDamageRend.enabled = toBeSeen;
        } else {
            if (myDamage != null) {
                myDamageRend.enabled = false;
            }
        }
Assets/NpcScript.cs GitLab
81
82
83
84
85
86
87
88
    public void UpdateMaterial(int currRound) {
        if (lastActiveRound >= currRound-1 && health > 0f) // one round leeway in case we catch the DB in mid-update
            renderer.material = liveMaterial;
        else {
            presumedDead = true;
            renderer.material = deadMaterial;
        }
    }

That presumedDead flag turns out to be pretty important: watching the database, I kept seeing NPCs that stopped updating with their hit points above zero. I’ll guess the server code doesn’t always update the hit points if a character dies in some interesting way.

Everybody hurts.

Bloody peasant!


Gone but not forgotten

You may have noticed the corpse disappearing in that last GIF. With this much bloodshed, the field was getting crowded pretty quickly, so it’s necessary to clean up the fallen:

Assets/Managers/NpcManager.cs in UpdateAllNpcs() GitLab
59
60
61
62
63
64
65
66
67
68
        foreach (NPCLite npc in npcs) {
            // skip inactive NPCs, and make sure they know they are inactive
            if (npc.lastActiveRound < maxGameRound - 10) {
                if (npcScripts.ContainsKey(npc.worldNpcID)) {
                    npcScripts[npc.worldNpcID].active = false;
                }
                if (npcLocations.ContainsKey(npc.worldNpcID)) {
                    npcLocations.Remove(npc.worldNpcID);
                }
            } else {

If you’ve been quiet for ten rounds, your active flag gets unset. And while it’s not set:

Assets/NpcScript.cs GitLab
35
36
37
38
39
40
41
42
          
    void Update () {
        // disappear and do nothing if inactive
        if (!active) {
            renderer.enabled = false;
            if (myDamageRend) myDamageRend.enabled = false;
            return;
        }

It works, but why not just destroy the objects? This is step one toward having an object pool of NPCs, so we don’t have to instantiate them and our memory churn is reduced. For now, keeping them around helps a little bit with debugging, at a small performance cost. Yes, and I now know I should have used SetActive.


Bonus Cautionary Tale: Earlier That Day…

And this was with the server side turned off. Nothing was changing, but memory usage would eventually skyrocket. Meanwhile the Unity editor ground to a halt and eventually had to have its process killed. I’ve been intentionally not paying much attention to memory allocation, planning to do a few future days on it, but this needed to be taken care of before we could do anything else. Shouldn’t take too long to figure out what’s going on, right?

1. Unity itself is locking up, and I am running an older version. Upgrade to v4.6.8 (latest 4.x version)…no change.

2. Try creating a standalone executable, and not having the Unity Editor running…no change.

3. Must be the garbage collector not geting a chance to do its job. Let’s invoke it every time we hit the database…

NpcManager.cs
1
2
3
4
5
6
void Update () {
        if (Time.time > nextDBUpdate) {
            print("Time to read from the DB! " + Time.time);
            float totalMemory = GC.GetTotalMemory(true);
            print("Total Memory at time " + Time.time + ": " + totalMemory);
            print("Avg increase per second: " + (totalMemory / Time.time));
…very little change. Memory continued to increase, average increase per second would quickly hit a floor that was way too high.

It gets worse.

4. Let’s not actually instantiate any NpcScript objects…very little change. Huh?

5. Let’s not call UpdateNpcCohab…no change.

6. Let’s not call UpdateAllNpcs at all after the first minute or so…memory behavior settles down, somewhat.

7. Let’s just stop hitting the database for NPC updates after the first minute or so…same settling. What’s happening?

Assets/DragonsSpine/DAL/DBNPC.cs GitLab
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
        public static List<NPC> GetAllNpcs()
        {
            List<NPC> npclist = new List<NPC>();
            
            using (SqlConnection tempConnection = DataAccess.GetSQLConnection())
            using (SqlStoredProcedure sp = new SqlStoredProcedure("prApp_LiveNPC_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)
                {
                    NPC npc = new NPC();
                    npc.worldNpcID = Convert.ToInt32(dr["uniqueId"]);
                    npc.lastActiveRound = Convert.ToInt32(dr["lastActiveRound"]);
                    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;
        }

When checking the database, on each data row, we’re instantiating a new NPC object. How is that a problem? Once we’re done with that list of NPCs (on exit from UpdateAllNpcs), the garbage collector will take care of it, right? Let’s take a look at the constructor we’re hitting:

Assets/DragonsSpine/GameObjects/GameLiving/NPC/NPC.cs GitLab
314
315
316
317
318
319
320
321
322
        public NPC()
        {
            this.IsPC = false;
            m_Owner = this;
            this.npcTimer = new Timer(DragonsSpineMain.MasterRoundInterval);
            this.npcTimer.AutoReset = true;
            this.npcTimer.Elapsed += new ElapsedEventHandler(NPCEvent);
            this.Brain = new Brain(this);
        }

And since it’s a subclass of Character:

Assets/DragonsSpine/GameObjects/GameLiving/Character/Character.cs GitLab
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
        public Character()
        {
            this.thirdRoundTimer = new Timer(DragonsSpineMain.MasterRoundInterval * 3);
            this.thirdRoundTimer.Elapsed += new ElapsedEventHandler(ThirdRoundEvent);
            this.thirdRoundTimer.AutoReset = true;
            this.thirdRoundTimer.Start();
            this.roundTimer = new Timer(DragonsSpineMain.MasterRoundInterval);
            this.roundTimer.Elapsed +=new ElapsedEventHandler(RoundEvent);
            this.roundTimer.AutoReset = true;
            this.roundTimer.Start();
            this.socket = null;
            this.PlayerID = -1;
            this.wearing = new List<Item>();
            this.sackList = new List<Item>();
            this.beltList = new List<Item>();
            this.lockerList = new List<Item>();
            this.seenList = new List<Character>();
            this.Name = "Nobody";
            this.inputBuffer = new byte[MAX_INPUT_LENGTH];
            this.inputCommandQueue = new Queue();
            this.outputQueue = new Queue();
            this.inputPos = 0;
            this.spellList = new IntStringMap();
        } 

Problem number one is that these are pretty big objects for me to instantiate just to hold a few values. Problem number two is that something about them (those Timers?) is keeping the garbage collector from taking care of them. Time to stop calling server code so cavalierly and make only what I need.

Assets/DragonsSpine/GameObjects/GameLiving/NPC/NPC.cs GitLab
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
 
    public class NPCLite
    {
        public int worldNpcID;
        public int lastActiveRound = 0;

        string name;
        public string Name
        {
            get { return this.name; }
            set { this.name = value; }
        }

        short xcord = 0;
        short ycord = 0;
        int zcord = 0;

Even this should be refactored, probably into a struct. But just making this simpler class (only the members I needed, no constructors inherited or otherwise) left me able to run for hours with no significant memory issues.


More to come

Day 16 code - client