What's better -- multiple scripts or public "read only" variables?

Hello,

I’ve come to the point where it’s time to add multitudes of new characters to my game-- each with their own set of stats, weapons, collision detection, animations, etc.

Right now I have two (well, three) choices on what to do, but I’m not quite sure what would be the “best.” (I use the word “best” loosely, as there probably will be differing opinions)

I started using Public variables as static, and I manually set the default values in the inspector. Right at instantiation of the object, I set them to private variables.

Because I’m going to have at least 50+ different characters, this was the best approach I could come up with. The next best was to make an Initialization script where I switch the object name and add stats that way.

My question: Is this even feasible, or should I start looking at something else?

Also… if it’s not obvious already, this is my first big project using an OOP language.

Thanks for the link, Fattie. Tbh I've never even looked at a pool until now.

Yes but the right answer isn't to tell them not to - it's to educate on how to do it right.

4 Answers

4

I will take a few moments to put my actual 2 cents in to how I think this should be done.

If objects are created in the scenes during design time, then I would assume that to mean that they are statically placed and that you don’t really have to track each on a global level.

Instead, you’ll handle each as they are acted upon. That means, that when a collider collides with an object, you’ll handle that object and appropriate message pumping through the collision event handler.

But - if you’re creating objects on the fly, you might or might not have to track them as a whole. This depends on their purpose and how they would interact. Two examples:

a) You create a bunch of “debris”. Let’s say this is an overhead rpg and you want some destructible barrels, pots and bushes. You don’t manually place them at design time because you want to make them random and so you place them all during the scene setup at runtime.

  • You probably don’t have to track any of this globally - you just run in to it as a player or attack it and if it connects, the appropriate physics object collision event will take care of destroying the object, creating a lootable if necessary and playing a sound. All is well with the world.
  • Since each of these items are randomly placed, you decide that you don’t need to save them out as game state. Therefor, there is no reason to track them globally.

b) On the other hand, you have a couple princesses and prices who need rescuing. You, the valliant adventurer are going to go through every castle, and determine if the prince[ss] is in “another castle” or not.

  • Along the way, you start rescuing some of them.

  • Each of them on its own can be interacted much in the same manner as the destructibles above.

  • However - you also need to know how many are rescued since this determines the result of some scripted sequences, game progress overall, etc etc.

  • I would add each to the “castle” level as appropriate as a game object, to be interacted with - just like the barrels.

  • BUT - I would also have a global state management class and yes, I would make it static. Here’s an example, with an object for the princesses and princes I’ll call “nobility”.

    public class GameState {
    private static GameState instance; // Capitalization, remember!
    private List nobles;

     private GameState() {}
     
     public static GameState Instance
     {
     	get {
     		if(instance == null){
     			instance = new GameState(); // Later, when you add loading and saving, you'd load here.
     		}
     		return instance;
     	}
     }
     public List Nobles {
     	get {
     		return this.nobles; // In non-speed-dependent programming, I'd do AsReadOnly, but not in a game engine. Since speed is of the essence, I'm breaking some OOP rules on purpose to avoid garbarge collection.
     	}
     }
    

    }

    public class NobilityManagement {
    public static void AddNoble(Nobility noble){
    GameState.Instance.Nobles.Add(noble); // This is a rule broken (OOP rule) - usually you’d never modify something directly that belongs to another class. But since it’s static, I do.
    }

     public static void RescueNoble(string name){
         List nobles = GameState.Instance.Nobles; // Same here...
     	for(int i = 0 ; i < nobles.Length; i++){
     		if(nobles*.Name == name) {*
    

_ nobles*.IsRescued == true;_
_
}_
_
}*_

* throw new ArgumentException(string.Format(“The noble {0} wasn’t found!”, name));*
* }*
* }*

* public class Nobility {*
* public enum Genders {*
* Male,*
* Female*
* }*
* public Genders Gender { get; set; }*
* public string Name { get; set; }*
* public bool IsRescued { get; set; }*
* }*
And there would of course be even more relevant information stored within.
Player health would be another aspect of GameState, along with its appropriate management class, just like NobilityManagement. You’d have stuff like “GetHealth” or “IsDead”, whatever is needed.
If you want to more strictly adhere to OOP recommendations, you can still use the GameState paradigm, but then create singletons for each of the items you’re tracking and manage them through the singletons. That’s up to you.
But that’s how I’d do it (generally).

Strangely, all my List(Nobility) became just plain List when I submitted this. Probably some sort of HTML protection. Everywhere above that says "List" is actually "List(Nobility)" But replace ( with an angle bracket and ) with the other angle bracket. (I haven't run in to this before I don't think.)

if I understood correctly you are talking about the different attributes of a character. I would suggest creating separate data type classes for each one. To me its no harm creating many class files. It keeps the data separate and benefit in the long run when the code base starts growing and become unmanageable. Its good to keep this distinction between entities in the beginning as compared to mixing up everything together and end up puking all over it in the end.

It will also be a lot easier to serialize/deserialize the data later in case you want to save/load the game state or network the game.

So from what you're saying, just so I understand, is that you would have one main class called "Stats" and then 1 class for each individual stat? So like Stats.Health and Stats.Energy?

yes. something along those line. So if you have a Stats class. You can have a separate Weapons class that contains all the attributes of a weapon like damage, decay, cooldown. and have it references in your Stats class. I will also have to disagree a little bit about not having any static variables. Unity uses static classes like GameObject, Input, Resources etc etc that really come in handy and everyone uses them all the time or most of the time. I do understand that they have to be implemented with some caution.

I think (and this is what I often do) the idea is, say you have several vars like gunRate, gunDmg, gunFlash, which you copy into all your character scripts. Instead, make a single class gunStats containing all those vars, and have each script declare public gunStats gun; The advantage is, typing "gun-dot" will show just gun variables, adding reloadTime to gunStats magically adds it to every script. Can also put common gun functions inside of gunStats (if(gun.canFire())...) The drawback is, if you aren't used to that style, head pain will increase.

I did take your advice (and am implementing it right now, actually) about the different Health, Damage, Armor, etc scripts and have them being children of a greater "Vitals" Script. Each stat has a current max armor and weapon property. As for the Gun Example, Owen, I do indeed have a Gun script that includes things such as reloadTime, rateOfFire, clipSize, etc. The only thing that I need to do is organize it a bit further by adding specific functions (at least a way of doing it) such as FireLaser, FireLiquid, etc. I just have "Fire" (and with proper organization that's probably all I need)

Before you decide on “The Way it Shall be Done,” start doing it any old way to get a feel for it.

Pick maybe 2 characters to add that are about the same idea as the 1st – like they all fire a projectile, using an Attack animation; and all have “charge to dash” (with different times and dash distances.) Swap in different projectiles, “bang” emitters, different rigs and animation… . Then pick one weird character to add (channels an attack beam?)

You’ll get a feel for whether you setting them at runtime makes more sense (are the stats in a file?) or if you like setting them as Public Inspector vars in prefabs (the Unity standard way.) You’ll see any problems with changing collider sizes for each, different length Run animations… . Finish them, to be sure there are no lurking problems.

For the oddball, you can try adding if(charType==3) ... to the single script. Or whether if(attackDelay==0) use beam attack code makes more sense. Maybe you’ll get a feel for how the IFs can grow too much, and Inheritance can help you swap in the bullet/beam attack code.

Thank you for your input. Back before I could even get this far I spent a lot of time "theorycrafting" with different ways to do what I want to accomplish. Basically what I was getting at, though, was IF setting public / serialized private variables in the inspector is even a plausible thing to do, or if it should be completely handled by way of script (switches, ifs, etc)

I prefer doing multiple scripts. This way you can easily make the changes to individual enemies/characters without having to worry about designing a one size fits all approach.

For example one type of enemy may always attack you if they are within a certain distance and have line of sight, running towards you.

Another enemy or “critter” may always run away from you if they are within a certain distance and/or line of sight. And they may have a ton of randomization that occurs to determine where they run. They may also use random animations while idle where some enemies might patrol.

You may want enemies to have different drop rates, or to even Start() at unique times.

Additionally adding sound sources may require different numbers of sounds depending on enemies, and you don’t want extra objects unless you are really need them.

Other enemies may require additional scripts like magic spells shooting lasers or fireballs etc.

I do like the above mentioned approach of just having a base class, and using inheritance sub classes for these kinds of things I just mentioned.

And using prefabs is definitely handy, especially for creating new scenes or an object for the first time.

Using multiple scripts also makes it very easy to identify what kind of object you are dealing with by using something like:

if (rayHitObject.GetComponent<EnemyCrazyBeastScript>())
{
// do logic here
//example
enemyCrazyBeastScript otherObjectsScriptCrazyBeast=rayHitObject.GetComponent<EnemyCrazyBeastScript>();

otherObjectsScriptCrazyBeast.enemyHealth += -playerDamage;

}

You should avoid using Static variables except for things that will only have one value ever within your program. Usually a Static variables data seems to be most useful if it needs to be accessed by multiple scripts. Usually things related to the “game state” as mentioned above. So things like how many levels you have unlocked, how many potions you have, your experience, what kind of armor your wearing etc. Many of my public static variables are tied to the PlayerPrefs and interact with them to save the games data.

The biggest downside of this method is that with so many scripts possibly running in one given scene you need to be extra careful that each individual script is not wasteful. Each function should only be called when absolutely necessary if possible. And you should always be thinking about ways to simplify or optimize your code if you are even slightly concerned about performance.

I generally try to keep all my variables on enemies/critters private and of course non-static since there are multiple enemies. Certain variables like enemyHealth could be public to allow easy access depending on how you are setting things up.

This is a pretty detailed opinion/answer which I’m not sure will even benefit the OP that much because they already sound like they know what they are doing. But I’m sure someone brand new to Unity/programming might find this useful :slight_smile: