5
\$\begingroup\$

I'm currently working to make a game, and it's a first for both of us.

We are at the point where we are starting to have a lot of scripts pile up, and are wondering about the resource management & resource impact on having a lot of small scripts running.

Is there any drawback to having multiple scripts only doing one thing, as opposed to one large script?

For example, one of our scripts only contains this following code, and is likely to not change, but all it does is destroy the object after it's hit twice;

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PartsController : MonoBehaviour
{
    int collisionCounter;
    void OnTriggerEnter2D(Collider2D other)
    {
        if (other.gameObject.tag == "Projectile")
        {
            collisionCounter += 1;
            if (collisionCounter >= 2)
            {
                Destroy(gameObject);
                Destroy(other.gameObject);
            }
        }
    }
}

Would we be better off trying to nest things like this into a larger script, instead of running several small ones?

\$\endgroup\$
4
  • 4
    \$\begingroup\$ "and is likely to not change" – Oh, my sweet summer child... \$\endgroup\$
    – Zano
    Nov 12 '21 at 15:13
  • 1
    \$\begingroup\$ The makers of indie hit Celeste used Unity and open-sourced their Player script. It's over 5000 lines long. Try to understand what it does :D \$\endgroup\$ Nov 12 '21 at 21:30
  • \$\begingroup\$ I want to be perfectly clear, that they made a really good game doesn't mean that their code is good - that script is, most definitely, a very difficult to read and understand god object. If anything is to be learned from that situation, it's that code shouldn't be overrated for games, and that things like game design, graphics, handling (and if you want to go commercial, advertising, sales etc) are very important an should not be neglected. \$\endgroup\$ Nov 12 '21 at 21:37
  • 1
    \$\begingroup\$ Hey, another (completely unrelated) tip: Write your ifs the other way around. if(other.gameObject.tag != "Projectile") return; and then below that all the rest that's inside the if currently. That prevents your code from creeping further and further to the right the more ifs you have. Takes a bit getting used to, but try it for a week and you'll probably come to like it a lot. Google "early returns" if you want to know more about it. (And by the way, respect, your example is very readable and early returns is the only thing I would change.) \$\endgroup\$ Nov 13 '21 at 9:27
5
\$\begingroup\$

Scripts take up some space in memory, but a larger script covering the same job would generally take up the same amount or more. So you don't save anything that way.

If you have many instances of different scripts and you combine them into a single instance of a master script, you reduce the memory overhead of each individual instance, but this is normally not something that breaks the bank. And you might increase the complexity and potential for bugs by combining many different responsibilities into one script this way.

Scripts with an Update (or FixedUpdate, LateUpdate, etc.) need those methods called every frame, and that method call cost adds up even if those methods don't do anything. But the example you've shown doesn't have such a method, so that doesn't apply here.

For the script example you've shown, its main runtime cost is in invoking its collision handler.

If you replaced this script with a different script that also does the same job, you'd still have to invoke that collision handler, so that cost would not decrease.

(Technically it's possible to get a savings here if the needed part of the larger script was already in the instruction cache, so you don't need to go fetch a new version of it from a different script. But if you have a project with lots of different behaviours used a few times each, then it's not especially likely that the needed snippet will reliably be in cache the next time you use it - so I don't expect that will benefit your case).

If that new larger script does other things in its collision handler, then you'd need to add some extra logic to check whether it should do those other things, or do just the things that this smaller script does. That extra logic could increase the runtime cost.

So, refactoring this into a bigger script is likely to be a pessimization.

If you want to optimize this script, there are two things you can do:

  1. Invoke the collision handler less often

  2. Do less work each time you invoke the collision handler

You can get both of these at once by using physics layers instead of tags to separate your projectiles. Create a "Projectile" physics layer, and a "Projectile Detector" physics layer. Put all your projectiles on the projectile layer, and this script's object on the detector layer. Set the collision flags so that the detector layer collides only with the projectile layer.

Then you can remove the tag check (which should have been using CompareTag() anyway, but that's beside the point). Only projectiles will be able to invoke this collision handler, and every other object will be ignored automatically.

So, fewer invocations, and less work checking inside each invocation.


In terms of general practice advice: I would say favour small scripts that do just one job. Small scripts with a simple defined purpose (Single Responsibility Principle) tend to be easy to reason about and maintain, and less prone to developing bugs than massive scripts that try to do a lot.

Profile your game regularly, and use that evidence to find whether there is a particular script that has an outsized impact on your performance, rather than being suspicious of scripts overall.

\$\endgroup\$
3
  • \$\begingroup\$ Thank you for the incredibly detailed answer. For clarification, this would work with layers I already set up, assuming I have "Enemy" and "PlayerProjectile" layers that interact with each other, correct? \$\endgroup\$
    – EvelynSays
    Nov 11 '21 at 17:11
  • \$\begingroup\$ That sounds like something you can try, and then post a more detailed question if it does not work as desired when you try it. I don't have enough visibility into how your game works to answer this in a comment. \$\endgroup\$
    – DMGregory
    Nov 11 '21 at 17:20
  • \$\begingroup\$ I'm not familiar with Unity's scripting engine, but I know that in Skyrim, the usual advice is for events to do as little as possible, set a flag variable and/or increment a counter, and then call RegisterForSingleUpdate (which asks the engine to invoke a callback at a later time) for any remaining processing which needs to happen. The upside of this is, if the event is getting absolutely hammered every frame, you still only trigger one callback, which can batch together all of the events and process them as a group. \$\endgroup\$
    – Kevin
    Nov 12 '21 at 21:41
4
\$\begingroup\$

Yes, having fewer components in the scene (which might or might not correlate with many distinct script files you have) means that Unity has less management overhead, so reducing components by integrating multiple features into one can be a performance optimization.

But performance is not all that matters. Unity favors a software design approach called composition over inheritance. The idea behind this design approach is that having many small classes which each do exactly one thing is better than having few large classes which handle everything a specific entity does. The main argument for this component-based approach is flexibility: Each entity is the sum of its components. Which means that it is very easy to create a new type of entity by simply combining existing components in a different way.

In the end it's a trade-off between performance and design clarity. And whenever you stand in front of such a trade-off decision, remember a well-known mantra in software development: Premature optimization is the root of all evil. It is very difficult to predict in advance which parts of your code will be the biggest performance hooks. So sacrificing code readability and maintainability for performance when you don't even know yet if the player is even going to notice that performance optimization is counter-productive.

So what you should usually do is prioritize design clarity until you actually have a perceivable performance problem and you used the profiler to find out which part of your code causes those problems.

\$\endgroup\$
2
  • 3
    \$\begingroup\$ "So sacrificing code readability and maintainability for performance when you don't even know yet if the player is even going to notice that performance optimization is counter-productive." ONCE MORE FOR EMPHASIS!! :) \$\endgroup\$
    – Almo
    Nov 11 '21 at 16:48
  • \$\begingroup\$ Thank you. I wasn't aware of the profiler, and this will be incredibly useful! \$\endgroup\$
    – EvelynSays
    Nov 11 '21 at 17:12
2
\$\begingroup\$

"It depends", as always in software development.

Unity has a component approach, and if you look at the built-in stuff, it generally follows the philosophy of having several scripts instead of one. So you have a collider component and a Rigidbody component instead of one "Physics Object" component - because it is possible that you want one but not the other.

One approach is to start with a general design principle of using scripts doing one thing, but watching out for indications that scripts should be lumped together into one, such as:

  • two different scripts communicating more than once with each other (via direct calls or messages)
  • one script accessing data stored on another script
  • two different script manipulating the same object data (such as position or rotation)
  • code starting to be duplicated between scripts, either exactly or very similar (i.e. something you could merge with an if or two)
  • two different scripts listening to the same input, be it player input, physics interactions (collisions, triggers, etc.) or messages from other scripts.

All of these and a few more are signs that this maybe should be one script instead of two, but there can still be good reasons to keep them separate, so it's always a case-by-case decision.

Note that some methods do indeed have a performance impact. Running Update on ten scripts is more expensive than running it on one, and that is true for many more functions. For objects you have once in your scene like the player object, that essentially doesn't matter, but on objects that can be in the scene hundreds of times, it can have an impact on performance.

\$\endgroup\$
0
\$\begingroup\$

I don't have any experience with game development, especially not with Unity, but... code is code, and I'm very well familiar with C#, so I think I can add my 2 cents.

In my opinion, this is a matter of taste. This same question - "fewer large files vs more small files" comes up in all sorts of programs. Performance is usually minimally affected, but readability and maintenance is a different story.

I've seen examples of both extremes. A project where no code file has less than a 1000 lines, and a project where almost every function has its own file. And I must say, both were hard to work with.

For large files, you often get lost in them and need to spend time finding your way back. Large files also tend to breed large methods which are also hard to keep in mind when reading code.

On the other hand, if you split your code into 1000 small pieces, that's also a challenge to comprehend. You only ever see a small part of the overall process you're investigating, and you need to keep jumping back and forth between files, trying to remember 1000 tiny pieces and how they all relate to each other.

It's a matter of taste, of course, but if I have to choose between these two extremes, I'd rather do the large files than the 1000 breadcrumbs.

Ideally though you want to strike a balance. Not too big, not too small.

Split and group functionality together so that each piece makes sense logically, not worrying too much about file size. This way, when you need to work on a certain piece, you can think about it in isolation, forgetting most of the rest of the system. And all the code for it should be close together, so that you don't need to travel far and wide trying to round up a zillion miniature particles.

And in turn when you use it, you don't need to think about how it does what it does - you can just treat it like a "black box of magic" that can do certain things.

Note that it's pretty difficult to judge these things while writing code. Writing code is easy, no matter how you split or merge it. When you have freshly written it, it's all in your mind and all seems easy. But come back to it after a year (or ask a colleague to look at it) and you'll start to get a sense of how well written it is. If it's well written and well split/grouped, you'll easily remember (or understand) what is what and where to look for the lines you're interested in.

\$\endgroup\$
-1
\$\begingroup\$

As has been often noted, efficiency-wise it's not going to matter. Spreading your code into X many scripts comes down to readability.

But making many small scripts, like that collision thing, is a pain. Looking through a Cow script that does everything Cows need isn't too bad. But with a small script for each Cow ability, having to remember the name of the script that does X to them is a pain; and having all of those scripts open in your editor is too many to fit.

It also makes communication more complicated. With one big script you can use "class globals" -- if 3 other Cow functions need to use your collisionCounter variable they can read it as-is. With lots of small scripts you need to find the other script with things such as GetComponent<BrickCollision>().collisionCounter.

So, in practice, make a large script for each type of gameObject, use #region, and when that gets messy split it into 2 or more.

\$\endgroup\$
1
  • \$\begingroup\$ Comments are not for extended discussion; this conversation has been moved to chat. \$\endgroup\$
    – DMGregory
    Nov 13 '21 at 18:44

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .