Back to Main Page


This page is an overview and full sample mod project, showing how to manage time in your project, to ensure you get maximum functionality, whilst maintaining acceptable performance.

 

Script Handling - Manage Your Time


I noticed some comments on a mod site yesterday about certain mods causing performance problems, apparently there has been some feedback about serious FPS drops. I had an idea which mods they were, so I downloaded one to see if I could see what the problem was. I confess, I nearly fell off my chair when I saw what the problem was.

These are simple business management mods, there should be minimal, if any, performance loss from this type of mod, unless something is very wrong. In the case of these mods, there is something seriously wrong. Let's consider the Executive Business mod. This mod alone is the equivalent of running 22 mods... in case you didn't quite get that, TWENTY-TWO mods. This is because virtually every class inherits from Script, meaning it runs like a separate mod. What makes it worse, is that the majority are sat there doing numerous World.GetDistance() checks. If you want to make someone's PC grind to a halt, World.GetDistance() is going to do it every time. It uses Square-Root calculations, which are incredibly expensive but more importantly, are completely unnecessary.

What also doesn't help is having multiple NativeUI MenuPools scattered across the mod, that are processed regardless of whether they are visible or not. No wonder people can only run one or two of these at a time... good lord. :-\ Is any part of the MenuPool visible? Yes -> Process it. No -> Don't process it. It's that simple.

Before I go any further, here's an absolutely critical tip: Are you releasing your mods in Release mode or Debug mode? If you can't answer that, check it now! If your answer is Debug mode, change it now! Debug mode bundles a whole heap of additional resources into your mod, that is designed to help with debugging. You neither need it, or want it. It will make your dll bigger and it will make your mod perform worse than it should do.

So as it is this type of mod causing the problem, I will explain this process in respect to how it would help that particular type of mod.

Let's start with some time management, because that's where the source of all these problems lie. Resource management mods are designed to be responsive to the passing of time, not immediate interaction with every possible business at any one time. It doesn't matter if that time is 1ms (millisecond), 30ms or 1000ms, a properly configured system will adapt to the period of time automatically.

Like a workplace, the best way to ensure people are managing their time properly, is with an efficient manager. One of the essential parts of this type of mod, is also, an efficient manager and typically, they are called manager classes. So in the case of this type of mod, you would have a BusinessManager class.

So to continue with the workplace analogy, the manager has two options when dealing with people, he can either:

1) Speak to everyone at once, stopping them all from what they were doing and causing chaos, because they will all be fighting for attention.

or

2) Deal with them individually, keeping things running as smoothly as possible and focusing on certain people when the need arises.

Number 2 is the process we should be following and that's what I am going to cover on this page.

So let's consider a situation where we have a mod that has 10 businesses and each business has 10 locations that are important to that business. So that's 100 locations, all of which are going to be distance checked... sounds a lot right?

Here's the main BusinessManager Class

using System.Collections.Generic;
using System.Drawing;
using GTA.Math;

namespace TimeManagementTest
{
    class BusinessManager
    {
        // This is the collection of businesses and a set of colours to identify the markers
        List<Business> BusinessPool;
        internal static List<Color> BusinessColours = new List<Color>() { Color.Red, Color.Orange,
            Color.Yellow, Color.LawnGreen, Color.Blue, Color.Magenta, Color.DarkOrchid, Color.Goldenrod,
            Color.Salmon, Color.Aqua };

        // These are the indexes that will be used with the business collection
        int PoolUpdateIndex;
        int PriorityPoolIndex;

        // How many businesses? We'll set it to 10
        const int MaxBusinesses = 10;

        /// <summary>
        /// Constructor for the main business manager class
        /// </summary>
        public BusinessManager()
        {
            CreateBusinesses();
        }

        /// <summary>
        /// Creates the pool of businesses
        /// </summary>
        void CreateBusinesses()
        {
            BusinessPool = new List<Business>();

            for (int i = 0; i < MaxBusinesses; i++)
            {
                Business eb = new Business(i);
                BusinessPool.Add(eb);
            }
        }

        /// <summary>
        /// Called from the main mod class and runs every frame
        /// </summary>
        public void Update()
        {
            // Check to see which business should be treated as the priority business
            CheckBusinessPriority();

            // If we have a priority business, update that every time
            BusinessPool[PriorityPoolIndex].Update();

            // If the current pool index is different to the priority pool index, update that business as well
            if (PoolUpdateIndex != PriorityPoolIndex)
            {
                BusinessPool[PoolUpdateIndex].Update();
            }

            PoolUpdateIndex = (PoolUpdateIndex + 1) % MaxBusinesses;

            BusinessPool[PriorityPoolIndex].DrawClosestMarker();

        }

        void CheckBusinessPriority()
        {
            float closestDistance = 999999999999999f;

            foreach (Business eb in BusinessPool)
            {
                if (eb.ClosestDistance < closestDistance)
                {
                    PriorityPoolIndex = eb.BusinessID;
                    closestDistance = eb.ClosestDistance;
                }
            }
        }
    }
}

There's nothing overly strange going on in there, it creates a pool of dummy businesses to give it something to work with.

In the Update() function, two main things are happening:

1) The business that is marked as a priority (i.e. the one that you are closest to) gets updated every frame. This ensures that if you're drawing menus or displaying help text, they remain on screen.

2) The other businesses are updated in turn, just to keep them ticking over for resource processing or anything else that evolves based on time passing.

After the updates, the pool index is advanced and wrapped based on the number of businesses and then whichever business is the priority, gets its closest marker drawn. The important thing to remember here, is that you can only be in one location at once and you can only ever be closest to one location at once, so focus the player's attention on that fact.

The CheckBusinessPriority() function goes through each business and compares their closest distances to see which is closest.

Now here's the Business Class

using System.Collections.Generic;
using System.Drawing;
using GTA;
using GTA.Math;

namespace TimeManagementTest
{
    class Business
    {
        List<Vector3> MarkerLocations;
        Vector3 MarkerScale = new Vector3(2.5f,2.5f,200f);

        public int LastGameTime;

        int ClosestlocationIndex;
        public float ClosestDistance;
        public int BusinessID;

        const int MaxLocations = 10;

        Color MarkerColour;

        public Business(int ID)
        {
            BusinessID = ID;
            CreateLocations();
            LastGameTime = Game.GameTime;
        }

        /// <summary>
        /// Create some dummy locations
        /// </summary>
        void CreateLocations()
        {
            MarkerLocations = new List<Vector3>();

            for (int i = 0; i < MaxLocations; i++)
            {
                float x = cTimeManagementTest.modRandomNumber.Next(-3000, 4000);
                float y = cTimeManagementTest.modRandomNumber.Next(-3500, 7100);
                float z = 30;

                MarkerLocations.Add(new Vector3(x, y, z));
            }

            MarkerColour = BusinessManager.BusinessColours[BusinessID];
        }

        /// <summary>
        /// Updates this Business
        /// </summary>
        public void Update()
        {
            // This tracks how much time has elapsed between the last and the current update in ms
            int currentGameTime = Game.GameTime;
            int elapsedGameTime = currentGameTime - LastGameTime;
            LastGameTime = currentGameTime;

            // After this, we will know exactly which location we are closest to and can deal with just that location
            CheckLocationDistances();
        }

        /// <summary>
        /// Do fast distance checks on the business locations
        /// </summary>
        void CheckLocationDistances()
        {
            ClosestDistance = 999999999999999f;
            ClosestlocationIndex = -1;

            for (int l = 0; l < MaxLocations; l++)
            {
                float distance = BusinessExtensions.FastCheckDistance(MarkerLocations[l]);

                if (distance < ClosestDistance)
                {
                    ClosestDistance = distance;
                    ClosestlocationIndex = l;
                }
            }
        }

        /// <summary>
        /// Draws a marker at each location
        /// </summary>
        public void DrawClosestMarker()
        {
            Vector3 markerLocation = MarkerLocations[ClosestlocationIndex];
            World.DrawMarker(MarkerType.VerticalCylinder, markerLocation,
                Vector3.Zero, Vector3.Zero, MarkerScale, MarkerColour);
        }
    }
}

The class gets created and is passed an ID, which is its index in the List in the BusinessManager class. It creates a bunch of random locations, just for the sake of having some locations to work with. It also gets its marker colour from the List of colours.

The first thing the Update() function does, is calculate how much time has passed since it was last updated. This will be a millisecond based value and can be used for all time based processing that needs to happen in that business.

Important: Time is the key to ensuring that all PCs run the mod the same. If you do Value = Value +1 (or Value++) every frame, what happens? On a PC running at 30fps, it adds 30 per second. At 60 fps, it adds 60 per second and for those playing at 144Hz... you can see the problem. If you get your elapsedGameTime and divide it by 1000, that gives you a percentage per second.

So if you want something to increase in value, by say 250 per second, then at 30fps, you will get 34 (average ms at 30fps is 32 - 34) / 1000 = .034, 250 * 0.34 = 8.5

At 60fps, it's 17 (average is 16 - 17) / 1000 = .017, 250 * .017 = 4.25

8.5 * 30 = 255, 4.25 * 60 = 255, so you can see that the different frame rates give you consistent values. Whilst they are higher than the 250 I stated, over the period of 1000ms, they will work out to be 250 as values shift around the average. This is why the businesses don't need to update every frame. If your game is running at 60fps and the business is only updated once every 10 frames, then it will accumulate that amount of elapsedGameTime. So instead of getting 16ms, it will get 160ms (approx). 160 / 1000 = .16, 250 *.16 = 40. So you can see how it adapts itself to changing time periods.

Another place where this time based percentage is important, is with movement. Using time based speed calculations will ensure things move at the same speed at different frame rates too. Basically, do things per-second, not per-frame.

The only other thing this class does, is to check the distances from each location to the player. It does this via an extension function in the final class.

using GTA.Math;

namespace TimeManagementTest
{
    public static class BusinessExtensions
    {
        /// <summary>
        /// Uses the fast Distance-Squared check to return the distance between the specified position and the player
        /// </summary>
        /// <param name="position"></param>
        /// <returns></returns>
        public static float FastCheckDistance(Vector3 position)
        {
            float dx = (position.X - cTimeManagementTest.PlayerPed.Position.X);
            float dy = (position.Y - cTimeManagementTest.PlayerPed.Position.Y);
            return (dx * dx) + (dy * dy);
        }
    }
}

This is the time-saver and is a major factor in keeping your FPS hit to a minimum. You just have to be aware that this is a 2-dimensional check based on X and Y values alone, if your location is above or below the player's level (like up some steps), then you might need to do some additional distance checking on the Z axis when you get close. You can even use the World.GetDistance() function, as you are only ever checking a single location now.

I normally use that function where it takes in two Vector3 parameters, I just simplified things for this demonstration. If you duplicate that function in the extensions class, change the input parameters to two Vector3s and then change cTimeManagementTest.PlayerPed.Position to the second Vector3, you will have both options to use.

Note: Extension classes, typically created as public static [classname], are classes that you don't create an instance of but have functions that can be accessed by every other class. I might add a page about them as they are a powerful tool and very useful.

How does this translate to in-game performance asks nobody? Let's take a look... it's a bit random because the locations are randomly generated each time the mod runs, so I have no idea where the locations are until a marker appears. I actually got fairly lucky in this video, all 100 locations could quite easily have been in the other end of the map.

 

So as you can see from this video, 10 businesses, 10 locations per business, full distance checking and nearest location marking, no visible impact on game performance.

Plus you might notice that the marker never disappears when you are nearby, which means the priority checking works perfectly, so your menus or help text won't disappear either.

If you are writing mods, the user's performance and stability are the absolute number one priority. Hopefully this page will give you some help to ensuring that it isn't your mod that ruins someones gaming experience.

Link to the project file for this demo mod: TimeManagementTest