I’ve been trying to think of the better of three methods to go about scripting Update calls for Ai turrets on ships.
The situation:
Ships can have from 1 to lets say 20 turrets installed at any given time. So the question becomes, does it make more sense to have each turret execute it’s functions via there own Update call or combine them into a single Update call inside a “Master Script”. There are also some external situations that need to be handled as well.
Before each shot, check if there is enough energy to fire from the energy pool on installed ship.
Each shot fired needs to subtract from an energy pool from the installed ship.
For non rigidbodies; be able to get a current velocity from it’s targets script.
Know the speed it’s installed ship is moving. (projectile velocity mod)
Installed Ship Speed Energy Pool <—> TURRET <— target speed
Method 1 - Every turret has it’s own Update:
This method of having each turret execute it’s function from it’s own Update is by far the easiest but i think also the most expensive. This method requires every turret to store a script reference to both the Ship it is currently installed (to check if enough energy to fire and updating the energy pool after fire), and a reference to it’s current target (to get it’s current speed for target lead).
Add in just a small number of ships and the number of update calls and information being passed between scripts can jump up by 100x very easily.
Method 2 - One Update per ship:
This method will contain the turret logic and store all the turrets information into an array on a “Master Script” and execute there functions in one Update. This “Master Script” would only need to store the targets speed for any given target. My problem with this method is that it will require information for every turret to be duplicated inside this “Master Script” Information such as
projectile
turret speed XY
Angle limits
Rate of fire
Turret
Barrel
Many more… 16 variables total. * number of turrets.
While it reduces the update calls and handles all other information required by the turrets inside a single script, it creates an overhead of duplicating values.
Method 3 - Hybrid:
This method would store all the turrets scripts references in an array inside a “Master Script” but would leave the turret logic functionality on the turrets them self’s. The “Master Script” would call each turrets function it’s self. Running all installed turrets in a single Update call but will still require that every turret stores it’s own references from method one or be fed this information from the “Master Script”.
While method 2 seams like it might be the better option, I was wondering what others might have to say. If there is another method that would work even better that i didn’t list then i would love to hear it.
Firstly, don’t make performance assumptions like this without testing to see if they’re actually right. I’d assume that method 2 would be the way to go, because you’re doing the same stuff in fewer scripts, which intuitively seems more efficient, but without testing how each usage pattern actually performs in Unity you just don’t know for sure.
Secondly, unless you have lots of ships it’s unlikely to make any practical difference. So I’d recommend designing for a useful editing pipeline as much as (or more than) for saving a few clock cycles. So I’d possibly suggest individual turret scripts so that you can edit them nicely in the editor instead of editing them indirectly through a parent object.
Thirdly, if it were me I’d probably go for “property scripts” on each turret so that they’re nice to edit, but then a “master script” to control the actual AI. This isn’t just for performance, it’s because it makes intuitive sense (to me) for a lot of the decision making to be centralised for the turret AI. For instance, if there’s no energy then none should fire, and target prioritisation and assignment can all be done at once (and should probably be centralised anyway). Once you take the latter into consideration, you already have everything that you need to aim and fire each turret right there anyway, so it’s trivial to do that in the same script and save having the engine manage a whole bunch of extra scripts to do it. The “property scripts” would be just to make my life in the editor more easy, and would be used solely to populate the "master script"s data on initialisation. After that they could even be deleted, though that’s unlikely to make much difference. They certainly shouldn’t have any per-frame functions (Update, OnGUI, etc.) though, as that defeats the point of them being data-only.
Thanks for your reply, I think my method 2 actually follows secondly thirdly advice quite closely.
Each turret has a script that contains it’s own unique properties ATM ( logic code for how the turret operates). What prefab to shoot, how fast too turn, how much energy per-shot, and what turret mesh to use. These are the variables that make a turret unique. Then the “Master Script” aka the ships Ai which will contain the logic code for how the turret operates, will then copy those values at runtime Awake() into an array for each turret. So any changes that need to be made to a turret can be done on a per-prefab basis and can easily be changed out.
I’ve done an initial test of Method 1 with 130 turrets by them self’s targeting a stationary object. Each turret running it’s own Update call with no script references. This cost around 8ms or .06 ms per. Looking forward, adding not only turret ai but ship ai, and missile ai. Then looking at how much information will be common between the them. However your right i haven’t tested Method 2 yet, but before performing a massive re-wright I just wanted to ask and see what others and there experience would have to say. Thanks for your advice i believe it has helped me decide to start work on implementing method 2.
Ignoring the performance impact, I’d suggest the 3rd solution, because you have better controll in which order the turrents will be fired.
If you use approach 1) and you run out of engergy in a shoot volley (say you have 10 cannons o the left side, but only enough energy for 5 of them), then 5 shots from 5 random (out of 10) cannons will be fired, since the script execution order is undefined, so the shots may look strange if 1, 3, 4, 7, 10th cannon will get fired.
But if you have a central script which call each turrents script in the correct order, you the cannons 1, 2, 3, 4 and 5 will fire, 6, 7, 8, 9 and 10 will not because there isn’t enough energy to fire it.
Interesting point. I think i have it covered in Method 2. As each turret’s information is stored in an array and thus the fire order 1 - X. Before each shot the turret logic on the “Master Script” goes through some checks.
Is the turret Enabled
Is the player controlling the turret
Does the turret have a target;
Does the turret have a line of sight (Don’t shoot my ship)
Is the target in range of the turret
Is the target in the minimum attack angle threshold for the turret
Is the turrets shoot timer = 0
Is there enough energy to fire the turret
Just focusing on the energy aspect. The script will compare the amount of energy available to the amount required by the turret.
If((available - required) > 0){
available -= required;
Shoot();
}
Since this method (when complete) will iterate through the turret list, once a condition (energy) is false no action will be available to that turret until it is once again true.
Thanks for your reply. It helps to make sure that i get everything covered.
I finished migrating to method two and the performance benefits are really good. Just to test i created a single ship with 130 turrets to emulate my 130 individual turret test from earlier.
Method 1:
130 individual update calls. 4ms with 8ms common fluctuation 12ms rare spike
Method 2
1 update call with 130 turrets on a single ship. 1.46 ms steady 7ms rare spike
I finished migrating to method two and the performance benefits are really good. Just to test i created a single ship with 130 turrets to emulate my 130 individual turret test from earlier.
Method 1:
130 individual update calls. 4ms with 8ms common fluctuation 12ms rare spike
Method 2
1 update call with 130 turrets on a single ship. 1.46 ms steady 7ms rare spike
That’s good to know. Was the script the only difference?
I’m surprised to see that the difference is so significant.
You mentioned that each turret contains its own “logic code”, does that mean you’re calling a function inside its script? If so, you might see another performance increase from moving that logic inside the main script and performing as much of it as you can inside tight loops. However, that’s a significant amount of effort, and I expect that the performance benefit would be small compared to what you’ve already achieved. It’s also quite possibly a case of optimisation where it’s not needed, since it might make your game logic harder to follow and yield little practical benefit. If it were my pet project I’d do it just for fun, but…
@angrypenguin: That’s exactly what I did. I moved all the turrets code into the “MasterScript”. From there I just looked for ways to optimize the code for its new environment. I was suprised my self, as i was not expecting that much if a gain. One of the reasons I wanted to do this was because, when I was working on my fps / 3rd person walker script. I was talking between three scripts. I found that once I brought all the code from those three scripts into one script that it had received a nice speed boost. I think I had reported the results in my fx_gdk post. From then I wanted to try to make any code that’s revelant to an object, into one script.
While the turret script its self is fine (just 0.06 ms per update), but add in multiple external references and turrets, then things start to climb fast. I just wanted to make sure I had made it the best I could because they have the potential to be very high in number.
This script is also running all ship related functions such as shield energy management, and will have the Ai. It also takes care of any of the ship’s equipment like reactor, main engines, thrusters, life support, sensors, ect. And now weapons.
There are other optimizations that I’m working on like grouped targeting. This would allow a group of turrets to share the same target information instead of fetching that information per each turret.
Be careful not to end up with a bunch of monolithic classes which are too huge to maintain. Remember that a MonoBehaviour script can still have its own internal objects which are not MonoBehaviours, though, so you can use that to keep organised.
Can you explain a bit more. Currently i have things broken up into four custom classes. I don’t see me needing any more.
class equipment{
}
class weapons{
}
class Ai{
}
class stats{
}
Equipment class holds all the gameObjects that make up the stats for the ship. Hull, Structure, Shields[ ], Capacitors[ ], LifeSupport, Sensors, ect
Weapons class holds all the weapons and there physical location on the ship.
Ai class holds all in formation for the Ai. Max attack range and Min attack angle, ect
Stats class holds the ships current condition for Armor, Shields, Energy, Thrust amounts.
This all gets executed inside 3 active functions and 2 support functions.
Without knowing any details, that sounds like it’s organised reasonably well and you should be ok. How it’s organised doesn’t matter so much, what’s important is that you’re keeping things maintainable instead of having a whole bunch of stuff in one big heap.