Large area - how many npcs dare I have?

I have quite a large town that’s supposed to be a capital city in my custom world. This is a prefab area I found and it looks great I think. It’s quite big (32x24). Now I need to populate the area. So far I have 40 NPCs around and it still doesn’t feel crowded at all. Since I have had problems with crashes in my previous modules, I don’t want that to happen here. How many NPCs dare I put in this area? I would want to put another 10 here at least, but I’m afraid of bugging the game down, so I’m not sure how I should proceed.

I am doing this one adventure in campaign mode with the campaign having 3 separate modules so far. The module with the city area only has this area. Again this is to create better stability for the game.

3 Likes

I just looked at what I think is my most populated big area it’s 24x 24 has 64 creatures, 969 environment objects, 105 lights and 109 trees.

I’ve also got one city area that is 12 x16 with 114 creatures, 1813 environment objects, 83 lights and 143 trees

Or a 12 x 16 with 197 creatures, 1155 environment objects, 399 trees and no lights.

Disclaimer… The last one was a massive fight, set at one time of day and when the locals saw the fighting a lot of them ran away so there were less of them by the end.

The toolset takes a while to load all of these but I reckon you’ve got a lot of “wriggle room” and can keep adding a lot more people.

2 Likes

A lot of the buildings in this area are not environement objects. I wonder if I have to go through most of them and convert them to that. It’s a real pain because I would have to draw a lot of walkmesh triggers and I quite despise that. Takes so much time…

1 Like

@andgalf

I wrote a post with some info and a script to show how objects in an area affect area load times, and thereby, general performance, I guess. It was aimed to help guide area sizes and content for MP games more than anything else, but it obviously has an impact on SP too. I’ll see if I can find the posts, which have data on how to consider the various objects types (inc creatures) when building an area.

Area Load Link.

Note there is a “Solution” link in the first post.

EDIT: Added the script used below.

2 Likes

I’m pretty sure that none of my buildings are environmental objects unless they’re off the map, the environmental objects are other things like barrels and tables etc.

To save drawing walkmesh triggers use the terrain editor and the unwalkable tab, just get as close to the objects as you can within the triangles. Be warned when you change a building into an environmental object the doors disappear, I have done that a lot when I select all the placeables at once and environment object the whole lot.

Either way I reckon you’re good to at least double your population just don’t have funky scripts on them, make a few walk waypoint paths for a bit of movement and you’re good to go.

2 Likes

@Tsongo - That actually sounds reassuring. I don’t know if I need to do some non-walk with the paint terrain tool, since the buildings that are placeables fix that by themselves.

I have already added 15 more people and at least at the moment when I test, everything seems fine. I only have stock scripts on them and I use ambient walking - X2_L_SPAWN_USE_AMBIENT - on a bunch. I might do some walk waypoint stuff too.

1 Like

The ambient spawn is good to create movement and you can set up single use triggers with cats,dogs, chickens etc or even rats… Just be sure they’re not hostile, make your own blueprint !

1 Like

I’m encountering crashes now when jumping between modules in my new adventure. It all worked so well until I began using this big area. I’m starting to think that me not spawning in the characters (I like to see them when I place them in my area but…) makes the crashes happen, but I don’t know.

@Tsongo - When you do your big areas, do you always spawn every character in?

Me too :wink:
Did you try with scripthidden set to true in the properties (and just make it false when the PC enter the area, back to true when they leave)?

2 Likes

Haven’t tried that. Does that help the engine much? I guess I could try that first…

EDIT: What the heck. I’ll just go and make the cliententer spawn everyone in, and then I guess I need to unspawn everyone when you leave, urggh.

EDIT 2: This will take forever…

1 Like

All my npcs are placed I only spawn in animals so I don’t think it’s that.

Copy your module and take out all the areas except for the big one, put the start thing in the area, rename it and jump straight in to see if it’s really that area before doing anything else. You can set up the jumping between modules too by switching the module name in the campaign to the new one and messing with waypoints.

Spawning the whole lot in seems like overkill and why would you get a crash, surely it would just slow right down when loading ?

2 Likes

andgalf and i have been working things over in PMs. I created a campaign with the huge prefab area, and another minimal module to load the two back and forth – without NPCs. It works fine for me so we think its the NPCs … and Ag is converting a bunch of static buildings (there’s a LOT of them) to environmental objects w/ walkmesh cutters …

/see what happens

ps. we narrowed things down to almost definitely the problem is in or with the large area

1 Like

Yep. Thanks for chiming in @Tsongo !

2 Likes

I have around 100 NPCs in the area I saw now. 35 of them erased for the time being. Will test spawning them later. After reducing the placeables by half and adding lots of walkmesh cutters I did a new test. The game still crashes like it did before.
I hope this won’t be the nail in the coffin for this adventure.

EDIT: When testing 2 times yesterday, not running the game fromt the toolset, the game actually didn’t crash. So I think my main problem with this area is the NPCs. To be on the safe side, I will erase all of them, place waypoints and spawn them in on the OnClientEnter and then erase them on the OnExit. That together with all the buildings I have remade as environment objects with walkmesh cutters will (I hope) solve this whole ordeal. Another thing that I think may be why my game crash is all the custom outfits and custom faces I have for my characters, even the NPCs. I hate the vanilla ones so I use a lot of faces from Jude Inc, Naku35 and Xaltar. This is perhaps straining when it comes to computer memory, especially when having so many NPCs in this area.

1 Like

@kevL_s

Did you still have access to that area load time script I wrote that gives feedback on objects that can affect an area load? (I posted a version of it again below, just in case it helps to shed any light on any specific objects.)

It might give some clue about some specific objects. (E.g. VFXs can often be a culprit, like torches.)

In my experience, we are aiming for a load time report result less than 30s if possible. (This is what I currently have a PASS/FAIL set at. It is set as a guide only though.) However up to a minute may be acceptable in some circumstances. Anything over a minute is starting to go into danger zones and potential fails in my opinion. NB: A SP game will obviously load these areas quicker, but the report timing values also give a good indication of the stability of the area in general.


const string CCBLUE		= "<color=CornFlowerBlue>"; 
const string CBLUE      = "<color=Blue>";
const string CRED    	= "<color=Red>";
const string CORANGE	= "<color=Orange>";
const string CDORANGE	= "<color=DarkOrange>";
const string CGREEN     = "<color=Green>";   

////////////////////////////////////////////////////////////////////////////////////////////
// ESTIMATE LOAD TIME FOR AREAS FOR A MP GAME (ALLOWS 4s FOR UNDETECTABLE SOUND OBJECTS)
// SET iDetailedReport TO TRUE (1) IF YOU WANT OBJECT DETAILS FOR THE AREA CHECKS
// SET iPlayerAreaOnly TO TRUE (1) IF YOU WANT TO CHECK THE PLAYERS CURRENT AREA ONLY
// WARNING! NEVER ALTER iStartFrom VALUE AS IT IS USED INTERNALLY BY THIS FUNCTION.
// ALSO, IF YOUR AREAS ARE LARGE AND/OR HAVE MANY OBJECTS, THEN THE LOOPS MAY FAIL. THIS
// CAN BE MADE MORE RELAIBLE BY REDUCING THE NUMBER OF AREAS LOOPED IN THE LINE THAT READS
// int iStopAt = iStartFrom + 3; < REDUCE THE NUMBER "3" UNTIL YOU GET A FULL REPORT IF NEED BE!
// NB: THIS SCRIPT USES COLOUR CONSTANTS THAT YOU WOULD EITHER NEED TO SETUP OR REMOVE
////////////////////////////////////////////////////////////////////////////////////////////
void MPEstimateAreaLoadTime(int iPlayerAreaOnly = 0, int iDetailedReport = 0, object oPlayer = OBJECT_SELF, int iStartFrom = 0, int iFAILED = 0, string sFAILED = "");
void MPEstimateAreaLoadTime(int iPlayerAreaOnly = 0, int iDetailedReport = 0, object oPlayer = OBJECT_SELF, int iStartFrom = 0, int iFAILED = 0, string sFAILED = "")
{
	float fLoadTime = 0.0;
	
	// OBJECT LOAD TIMES (CHANGE AS EACH TEST IMPROVES)
	float fCREINV = 0.07;		
	float fPLCINV = 0.15;		
	float fPLCUSE = 0.24;		
	float fDOORS = 0.11;			
	float fVFX = 0.05;			
	float fLIGHTS = 0.1;	
	
	int iPlayers = 0; int iPCs = 0;
	
	object oFM = GetFirstFactionMember(oPlayer, FALSE);
	
	while(oFM != OBJECT_INVALID)
	{
		if(GetIsPC(oFM)){fLoadTime = fLoadTime + 2.5; iPlayers = iPlayers + 1;}
		else{fLoadTime = fLoadTime + 1.0; iPCs = iPCs + 1;}
			
		oFM = GetNextFactionMember(oPlayer, FALSE);
	}
	
	int iREPORT = 1; int iQTYAREAS = iStartFrom;
	
	int iQTYITEMS = 0; 
	
	if(iStartFrom == 0)
	{
		SendMessageToPC(oPlayer, CBLUE + "<<< STARTING MP AREA LOAD TIME REPORT FOR MODULE (ALLOWS 4s FOR SOUNDS) >>>");
		SendMessageToPC(oPlayer, CBLUE + "<<< PLAYERS x " + IntToString(iPlayers) + " (PARTY MEMBERS x " + IntToString(iPCs)  +") = " + FloatToString(fLoadTime, 4, 2) + "s >>>");
	}
	
	object oAreaPC = GetArea(oPlayer);	
	
	// CHANGE STARTFROM TO A VALUE WHERE AREA LOOP STARTS	 
	// SET AT NUMBER OF AREAS TO COVER (USE MAX 3 - DECREASE IF NEEDED)
	int iStopAt = iStartFrom + 1; 	
			
	int iCount = 0; 
	
	object oAreaLoop = OBJECT_INVALID;
	
	if(iPlayerAreaOnly == 1){oAreaLoop = GetArea(oPlayer);}
		
	else
	{	
		oAreaLoop = GetFirstArea();
	}
	
	// MAX AROUND 15 AT A TIME, SO USE 1, 21
	while(oAreaLoop != OBJECT_INVALID)
	{	
		iCount = iCount + 1;
		
		// IGNORE THE FOLLOWING
		int iIGNORE = 0;if(GetLocalInt(oAreaLoop, "IGNORELOOP")){iIGNORE = 1;}
		
		// AREA DETAILS COLLECTION
		if(iCount > iStartFrom && iCount <= iStopAt && !iIGNORE)
		{		
			string sAreaTag = GetTag(oAreaLoop);
			string sAreaName = GetName(oAreaLoop);	// USE NAME AS NESEWERS TAG IS THREE AREAS!			
			
			string sAreaInfo = sAreaTag + " (TAG) " + sAreaName + " (NAME)";
			
			DelayCommand(0.4, SendMessageToPC(oPlayer, IntToString(iCount) + ") TESTING AREA > " + sAreaInfo));
			
			iQTYAREAS = iQTYAREAS + 1;
			
			string sCOLOUR = CRED;
			
			// AREA SPECIFIC TIMINGS
			float fTOTAL = 0.0;
			
			float fInvLoadTime = 0.0;		int iQPI = 0; 	string sQPI = "";
			float fUseLoadTime = 0.0;		int iQPU = 0;	string sQPU = "";
			float fCreLoadTime = 0.0;		int iQCI = 0;	string sQCI = "";
			float fDoorLoadTime = 0.0;		int iQD = 0;	string sQD = "";
			float fLightLoadTime = 0.0;		int iQL = 0;	string sQL = "";
			float fVFXLoadTime = 0.0;		int iQV = 0;	string sQV = "";
			//float fSOUNDLoadTime = 0.0;		int iQS = 0; // CANNOT DETECT
			
			string sITEMREPORT = "";		
			string sINVREPORT = "";	
			string sUSEREPORT = "";	
			string sCREREPORT = "";	
			string sDOORREPORT = "";	
			string sLIGHTREPORT = "";
			string sVFXREPORT = "";
			string sSOUNDREPORT = "";			
							
			// FIND ALL ITEMS
			object oItem = GetFirstObjectInArea(oAreaLoop);		
			
			while(oItem != OBJECT_INVALID)
			{
				int iTYPE = GetObjectType(oItem);
				string sTAG = GetTag(oItem);		
				
				////////////////////////////////////////////////////////////////////////////////////////////
				// ITEM CHECKS - BASIC STRAY ITEMS SHOULD NOT HAVE ANY UPON RELEASE!
				////////////////////////////////////////////////////////////////////////////////////////////
				if(iTYPE == OBJECT_TYPE_ITEM)
				{
					if(iDetailedReport){SendMessageToPC(oPlayer, CRED + "ERROR: ITEM FOUND IN AREA: " + sAreaInfo);}
					
					iQTYITEMS = iQTYITEMS + 1;
					
					sITEMREPORT = CRED + "AREA TOTAL ITEM OBJECTS > " + IntToString(iQTYITEMS);
					
					iFAILED = 1;
				}
				
				////////////////////////////////////////////////////////////////////////////////////////////
				// CREATURE CHECKS 
				////////////////////////////////////////////////////////////////////////////////////////////
				if(iTYPE == OBJECT_TYPE_CREATURE)
				{							
					if(GetFirstItemInInventory(oItem) != OBJECT_INVALID || GetItemInSlot(INVENTORY_SLOT_RIGHTHAND, oItem) != OBJECT_INVALID
					|| GetItemInSlot(INVENTORY_SLOT_CHEST, oItem) != OBJECT_INVALID)
					{
						//if(iDetailedReport){SendMessageToPC(oPlayer, CORANGE + "CREATURE " + "(" + (sTAG) + ") > FOUND IN AREA: " + sAreaInfo);}						
							
						fCreLoadTime = fCreLoadTime + fCREINV;		iQCI = iQCI + 1;
					}
					
					sCOLOUR = CRED; if(fCreLoadTime < 5.0){sCOLOUR = CORANGE;}
					
					sCREREPORT = sCOLOUR + "AREA CREATURE OBJECTS (" + IntToString(iQCI) + ") LOAD TIME > " + FloatToString(fCreLoadTime, 4, 2);
				}
				
				////////////////////////////////////////////////////////////////////////////////////////////
				// PLACEABLE CHECKS
				////////////////////////////////////////////////////////////////////////////////////////////
				else if(iTYPE == OBJECT_TYPE_PLACEABLE)
				{
					if(GetHasInventory(oItem))
					{						
						if(iDetailedReport){SendMessageToPC(oPlayer, CORANGE + "INV PLACEABLE " + "(" + (sTAG) + ") > FOUND IN AREA: " + sAreaInfo);}
						
						fInvLoadTime = fInvLoadTime + fPLCINV; 	iQPI = iQPI + 1;
						
						sCOLOUR = CRED; if(fInvLoadTime < 15.0){sCOLOUR = CORANGE;}
						
						sINVREPORT = sCOLOUR + "AREA INVENTORY OBJECTS (" + IntToString(iQPI) + ") LOAD TIME > " + FloatToString(fInvLoadTime, 4, 2);
					}
					
					else if(GetUseableFlag(oItem))
					{						
						if(iDetailedReport){DelayCommand(0.1, SendMessageToPC(oPlayer, CDORANGE + "USE PLACEABLE " + "(" + (sTAG) + ") > FOUND IN AREA: " + sAreaInfo));}
						
						fUseLoadTime = fUseLoadTime + fPLCUSE;		iQPU = iQPU + 1;
						
						sCOLOUR = CRED; if(fUseLoadTime < 15.0){sCOLOUR = CORANGE;}
						
						sUSEREPORT = sCOLOUR + "AREA USEABLE OBJECTS (" + IntToString(iQPU) + ") LOAD TIME > " + FloatToString(fUseLoadTime, 4, 2);
					}
				}
				
				////////////////////////////////////////////////////////////////////////////////////////////
				// DOOR CHECKS
				////////////////////////////////////////////////////////////////////////////////////////////
				else if(iTYPE == OBJECT_TYPE_DOOR)
				{
					//if(iDetailedReport){SendMessageToPC(oPlayer, CORANGE + "DOOR " + "(" + (sTAG) + ") > FOUND IN AREA: " + sAreaInfo);}
						
					fDoorLoadTime = fDoorLoadTime + fDOORS;		iQD = iQD + 1;
					
					sDOORREPORT = "AREA DOOR OBJECTS (" + IntToString(iQD) + ") LOAD TIME > " + FloatToString(fDoorLoadTime, 4, 2);
				}	
				
				////////////////////////////////////////////////////////////////////////////////////////////
				// LIGHT CHECKS
				////////////////////////////////////////////////////////////////////////////////////////////
				else if(iTYPE == OBJECT_TYPE_LIGHT)
				{
					//if(iDetailedReport){SendMessageToPC(oPlayer, CORANGE + "LIGHT " + "(" + (sTAG) + ") > FOUND IN AREA: " + sAreaInfo);}
					
					fLightLoadTime = fLightLoadTime + fLIGHTS;		iQL = iQL + 1;
					
					sCOLOUR = CRED; if(fLightLoadTime < 10.0){sCOLOUR = CORANGE;}
					
					sLIGHTREPORT = sCOLOUR + "AREA LIGHT OBJECTS (" + IntToString(iQL) + ") LOAD TIME > " + FloatToString(fLightLoadTime, 4, 2);
				}		
				
				////////////////////////////////////////////////////////////////////////////////////////////
				// VFX CHECKS
				////////////////////////////////////////////////////////////////////////////////////////////
				else if(iTYPE == OBJECT_TYPE_PLACED_EFFECT)
				{
					//if(iDetailedReport){SendMessageToPC(oPlayer, CORANGE + "VFX " + "(" + (sTAG) + ") > FOUND IN AREA: " + sAreaInfo);}
						
					fVFXLoadTime = fVFXLoadTime + fVFX;		iQV = iQV + 1;
					
					sCOLOUR = CRED; if(fVFXLoadTime < 10.0){sCOLOUR = CORANGE;}
					
					sVFXREPORT = sCOLOUR + "AREA VFX OBJECTS (" + IntToString(iQV) + ") LOAD TIME > " + FloatToString(fVFXLoadTime, 4, 2);
				}				
				
				oItem = GetNextObjectInArea(oAreaLoop);		
				
			} // END VALID OBJECT IN AREA			
				
			///////////////////////////////////////////////////////////////////////////////////////////////
			// AREA TOTAL REPORTS
			///////////////////////////////////////////////////////////////////////////////////////////////
			
			// STRAY ITEMS
			if(sITEMREPORT != ""){DelayCommand(0.5, SendMessageToPC(oPlayer, sITEMREPORT));}
				
			// CREATURE OBJECTS
			if(sCREREPORT != ""){DelayCommand(0.5, SendMessageToPC(oPlayer, sCREREPORT));}	
				
			// INVENTORY OBJECTS
			if(sINVREPORT != ""){DelayCommand(0.5, SendMessageToPC(oPlayer, sINVREPORT));}
				
			// INVENTORY OBJECTS
			if(sUSEREPORT != ""){DelayCommand(0.5, SendMessageToPC(oPlayer, sUSEREPORT));}
				
			// DOOR OBJECTS
			if(sDOORREPORT != ""){DelayCommand(0.5, SendMessageToPC(oPlayer, sDOORREPORT));}	
				
			// VFX OBJECTS
			if(sVFXREPORT != ""){DelayCommand(0.5, SendMessageToPC(oPlayer, sVFXREPORT));}	
				
			// LIGHT OBJECTS
			if(sLIGHTREPORT != ""){DelayCommand(0.5, SendMessageToPC(oPlayer, sLIGHTREPORT));}	
				
			// SOUND OBJECTS
			//if(sSOUNDREPORT != ""){SendMessageToPC(oPlayer, sLIGHTREPORT);}	
			
			// AREA TOTAL LOAD TIME (ALLOWING 5s FOR SOUND OBJECTS THAT CANNOT BE DETECTED)
			fTOTAL = 4.0 + fLoadTime + fInvLoadTime + fUseLoadTime + fCreLoadTime + fDoorLoadTime + fLightLoadTime + fVFXLoadTime;
			string sALERT = CRED + " FAIL !!!!"; if(fTOTAL < 30.0){sALERT = CGREEN + " PASS !!!!";}
				
			// LIST ALL FAILED
			else
			{
				sFAILED = sFAILED + "\n" + sAreaInfo + " : " + FloatToString(fTOTAL-4.0, 4, 2) + " TO " + FloatToString(fTOTAL, 4, 2) + "s ";	
			}
			
			DelayCommand(0.51, SendMessageToPC(oPlayer, CCBLUE + "TOTAL ESTIMATED LOAD TIME FOR " + sAreaInfo + " : " + FloatToString(fTOTAL-4.0, 4, 2) + " TO " + FloatToString(fTOTAL, 4, 2) + "s " + sALERT));
			
					
		} // END IF COUNT
		
		else if(oAreaLoop != OBJECT_INVALID && iCount > iStopAt)
		{
			iREPORT = 0;
			DelayCommand(0.2, MPEstimateAreaLoadTime(iPlayerAreaOnly, iDetailedReport, oPlayer, iStopAt, iFAILED, sFAILED)); oAreaLoop = OBJECT_INVALID; break;
		}			
		
		if(iPlayerAreaOnly == 0)
		{
			oAreaLoop = GetNextArea();
		}
		
		else
		{
			oAreaLoop = OBJECT_INVALID;
		}
		
	}	// END WHILE
	
	// END REPORT (MODULE)
	if(iREPORT == 1)
	{
		if(iFAILED == 0){DelayCommand(0.6, SendMessageToPC(oPlayer, CCBLUE + "<<< GOOD! NO STRAY ITEMS LEFT WITHIN MODULE! >>>"));}
		else{DelayCommand(0.6,SendMessageToPC(oPlayer, CRED + "<<< BAD! STRAY ITEMS LEFT WITHIN MODULE! >>>"));}
								
		DelayCommand(0.6, SendMessageToPC(oPlayer, CBLUE + "<<< " + IntToString(iQTYAREAS) + " AREAS CHECKED >>>"));
		DelayCommand(0.6, SendMessageToPC(oPlayer, CBLUE + "<<< REPORT FINISHED >>>"));		
		
		DelayCommand(0.7, SendMessageToPC(oPlayer, CRED + sFAILED));	
	}
}

////////////////////////////////////////////////////////////////////////////////////////////
// PATCH SCRIPT
////////////////////////////////////////////////////////////////////////////////////////////
void main(int iPlayerAreaOnly = 0, int iDetailedReport = 0)
{ 	
	object oPlayer = OBJECT_SELF;
	//object oHost = GetTheGameHost();
	object oHost = GetFirstPC();
	object oArea = GetArea(oPlayer);	
		
	MPEstimateAreaLoadTime(iPlayerAreaOnly, iDetailedReport, oPlayer, 0);
}
1 Like

@andgalf

I never run/test a game from the toolset functionality due to this reason. :slight_smile:

That facility (to run from the toolset) may work for smaller setups, but once you start to have more involved areas, then it is simply not reliable enough and can (often does) fail. I recommend always testing from a fresh game, just to avoid any potential differences that testing direct from the toolset can do.

2 Likes

I agree with Mr Botelle and never test from the toolset.

Also it could be all your custom content I don’t use it much… Especially not on peasants ! That should be easy to test, just pull the files out of your override and send them back to vanilla land.

With cities this sort of thing is probably why they’re nearly always done in districts, why not chop it up and have a few smaller areas ?

It might seem harsh but in the long run you probably save time because it’s easier to load, find things and adjust etc. You can also go way more wild with lights and placeables etc.

1 Like

+2

The area is extra huge. Ag says that its not crashing if no NPCs are there (render time is slightly laggy for me though). So the prob is the NPCs … am going to wipe all their AI-event slots (as they spawn) and see if it helps

1 Like

I made some changes in later versions of my AI to help with NPCs causing performance issues. I did things like preventing a huge queue of actions piling up when walking waypoints to turning off ambient animations and other actions if no PCs were within 50 meters. @kevL_s ’ test should see these changes could help.

3 Likes

Did some tests with @kevL_s new script and it looks very promising. Two tests and it went very smoothly when he made the NPCs dumb. So I think you might be right @TonyK since most of the NPCs has ambient animations on. Probably some action queue going haywire.