Edit: I've also changed the main menu to something more suitable for a horror game. Check it out
here.
In one of my DevLogs I showed some stress testing. I wasn't happy how laggy the game got after adding a bunch of NPCs on the map so I thought I'll optimize some more.
Here's where I started. This was what I was supposed to beat. 20 NPCs roaming around in the map.
Full update takes 30ms.. meaning I could crunch out ~ 33 frames per second.
This clearly needed fixing. So what I did was that I...
- Added a delay to entity update so the entities will only do certain secondary things every 400 ms instead of every frame
- Added a delay to NPC logic so they wont do secondary targeting tasks every frame but instead every 400 ms
- Changed pathfinder so it will return the path it has already calculated if it runs out of iterations.
Added a player variable to the level so if something has to call the player they don't have to go through every object in the game - Disabled player hallucination & health effect since it sometimes "hanged up" and caused almost 20ms spikes.
- Changed NPC rules from array to bitflags
After doing all of this I got these statistics out; do note a few more options have been added.
Nothing I did seemed to affect anything so I continued hunting for the milliseconds.
I added few more timers and noticed that pathfinding took almost all if not all of the object update time and I adventured further into the sources.
I noticed that each pathfinding task took from 1 to about 15 ms to do so I did what shouldve been an obvious choice from the getgo. I made the pathfinder work so that it can only find one path per frame.
This means that if I have 80 NPCs spawned on the map they'll take ~3.2 seconds for all of them to get paths.
There is one problem though. If a few of the NPCs only travel on a short path they might clog up the system and the last of the NPCs on the list might never get a path at all.
This problem can be remedied with good map design and I'm fairly sure I'll never even have more than 10 NPCs roaming around anyways.
Here's a screenshot of the game after optimization. 60 NPCs and game runs at 16 fps. Booyyah!
You're just going to have to trust me on this one but without the graph and stats the game runs at 22fps. Hooray for optimization!
Here's same stats as last time
80 NPCs = 15fps
60 NPCs = 22fps
40 NPCs = 32fps
20 NPCs = 42fps
10 NPCs = 50fps
0 NPCs = 52fps
Compared to the last stats I got the improvement is huge
!
Also if you're interested in the graph class you can grab it here. Note that you will need to supply your own text rendering/alter how the texts are shown.
TimeGraph.as
package utils
{
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.geom.Point;
/**
* ...
* @author zamp
*/
public class TimeGraph
{
static private var m_timers:Vector.<TimeGraphTimer> = new Vector.<TimeGraphTimer>;
static private var m_graph:BitmapData = null;
static private var m_highestGraph:Number = 100;
static public function time(name:String):uint
{
for each (var t:TimeGraphTimer in m_timers)
if (t.name == name)
return t.count();
// not listed yet
m_timers.push(new TimeGraphTimer(name));
return m_timers[m_timers.length-1].count();
}
static public function get graph():Bitmap
{
if (m_graph == null)
m_graph = new BitmapData(256, 128, false, 0x000000);
return new Bitmap(m_graph);
}
static public function updateGraph():void
{
if (m_graph != null)
{
// draw graph on itself but offset by -1x
var temp:BitmapData = m_graph.clone();
m_graph.fillRect(m_graph.rect, 0x000000);
m_graph.copyPixels(temp, m_graph.rect, new Point( -1, 0));
// draw timer averages on the graph
var n:uint = 0;
for each (var t:TimeGraphTimer in m_timers)
{
m_graph.setPixel(255, Math.floor(m_graph.height - ((t.time / m_highestGraph) * m_graph.height)), t.color);
Text.renderToScreen(t.name + " " + t.time + " ("+t.timeLow+"/"+t.timeHigh+")", 260, n * 14, "left", t.color);
n++;
}
}
}
}
}
TimeGraphTimer.as
package utils
{
import flash.utils.getTimer;
/**
* ...
* @author zamp
*/
public class TimeGraphTimer
{
private var m_name:String;
private var m_color:uint = 0;
private var m_count:uint = 0;
private var m_time:uint = 0;
private var m_timeHigh:int = 0;
private var m_timeLow:int = 0;
private var m_peakHigh:uint = 0;
private var m_peakLow:uint = 0;
public function TimeGraphTimer(name:String)
{
m_name = name;
m_color = Math.random() * 0xFFFFFF;
}
public function count():uint
{
if (m_peakHigh > 0)
m_peakHigh--;
else
m_timeHigh--;
if (m_peakLow > 0)
m_peakLow--;
else
m_timeLow++;
if (m_count > 0)
{
m_time = (getTimer() - m_count);
if (m_time > m_timeHigh)
{
m_peakHigh = 50;
m_timeHigh = m_time;
}
if (m_time < m_timeLow)
{
m_peakLow = 50;
m_timeLow = m_time;
}
m_count = 0;
} else { // not yet counting
m_count = getTimer();
}
return 0;
}
public function get name():String
{
return m_name;
}
public function get color():uint
{
return m_color;
}
public function get time():uint
{
return m_time;
}
public function get timeLow():uint
{
return m_timeLow;
}
public function get timeHigh():uint
{
return m_timeHigh;
}
}
}
Usage example:
// add this in your Main class
addChild(TimeGraph.graph);
// add this in your main loop
TimeGraph.updateGraph();
// call this function twice with the same string to get how long it took
// this string is also the name that shows up besides the graph
TimeGraph.time("foo");
// do something here
var timeItTookInMs:uint = TimeGraph.time("foo");