7
\$\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
  • 5
    \$\begingroup\$ "and is likely to not change" – Oh, my sweet summer child... \$\endgroup\$
    – Zano
    Commented Nov 12, 2021 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\$ Commented Nov 12, 2021 at 21:30
  • 1
    \$\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\$ Commented Nov 12, 2021 at 21:37
  • 3
    \$\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\$ Commented Nov 13, 2021 at 9:27

7 Answers 7

7
\$\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
    Commented Nov 11, 2021 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
    Commented Nov 11, 2021 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
    Commented Nov 12, 2021 at 21:41
7
\$\begingroup\$

Yes, having fewer components in the scene (which might or might not correlate with how 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. For example, when you have a Hitpoints component which you put on anything that can be attacked and destroyed, then you can make any object in your game destructible just by adding that Hitpoints component to it.

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
  • 4
    \$\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
    Commented Nov 11, 2021 at 16:48
  • 1
    \$\begingroup\$ Thank you. I wasn't aware of the profiler, and this will be incredibly useful! \$\endgroup\$
    – EvelynSays
    Commented Nov 11, 2021 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\$
1
\$\begingroup\$

The short answer:

It's better (in my opinion) to use lot's of small scripts, having looked through lot's of decompiled games from professional game dev companies, this seems to be the way to go. The reason is pretty simple, if you have lots of small scripts it's easy to find what you're looking for without having to take a deep dive into one large script.

However, it may not be that simple for you.

Let's break it down into 2 key points: preference and efficiency.

Preference:

Obviously, it's up to you to decide which you prefer, research what benefits you get out of both options (although I'll tell you there aren't many benefits or drawbacks to either approach)

Find which method suits you best, feels good, and just feels right. If you have a higher efficiency writing it in separate scripts, but it makes it less enjoyable, use one large script. Over the years I've spent doing game development, the biggest thing I've realised, is that it doesn't matter how you do something, as long as it works, and is enjoyable to write. That being said, usually the more efficient solution is more enjoyable, as you get a faster, more satisfactory result.

Efficiency:

If you find yourself wasting time looking for the right script in a folder that contains hundreds, you have a low efficiency. When it comes to Game Dev, having a high efficiency is key. Alternatively, if your struggling to find a function inside your 10000 line giant file. That would also be inefficient.

Since you're using Unity having the scripts interact with each other is simple, since Unity automatically compiles all scripts into the same assembly and some other stuff I'm not so sure about, compared to something like C++ which can get really annoying when trying to include different files, Unity makes it really easy. If that weren't the case, then it may be worth using larger scripts.

I hope this answers your question clearly.

\$\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\$
0
\$\begingroup\$

You will probably be more productive if initially you just write back to back, and then worry about taking out the common functions and organizing it into smaller scripts later on. I would say write it so it works then consider refactoring it later.

Use ctrlf to jump to parts of the script that need your attention, rather than fumble with open tabs.

\$\endgroup\$
1
  • \$\begingroup\$ The second part of your argument is not really valid since with ctrl + shift + f you can search just as well between multiple files. And even more you would just go with ctrl + left click to the functions. \$\endgroup\$
    – Zibelas
    Commented Feb 29 at 17:46
-2
\$\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
    Commented Nov 13, 2021 at 18:44

You must log in to answer this question.

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