When I program a character or a monster I usually have few scripts attached to the gameObject and I always have one that control almost everything. In the zombie game I’m doing I have a player script attached to my player that handles almost everything, health, movement, input, etc. The only thing that is not in this script is the inventory system.
I know this is not the right way to do things, but the main problem to me when dividing this “main” script is with the dependencies, I watch some videos explaining how I could split my script and most of them just created the specifics scripts and then attached each one of these scripts to the script that store the variables (or something similar to this).
So then what happens if the input script doesnt have the reference to the movement script because my main script want to move the player or something like this.
I know how to use methods of another class or change its variables, but it seems much simpler to just having one handling everything because then I dont need to reference everything.
So anyone have another idea on how to split logic into different scripts? What about scriptable Objects? Im already using them but should I use them to storage inputs or something like that? How should I do that? Give me your ideas!
Hope this helps, i’ll make a super basic script and then split it into 2, as an example I’ll make a script that handles HP and movespeed in a super rudimentary way.
public class Playerscript : Monobehavior
{
public float moveSpeed;
public float health;
void FixedUpdate(){
MoveForward();
}
void MoveForward(){
transform.position.x += moveSpeed; //let's ignore that this doesn't actually work like this i'm just being lazy
}
So if we want to extract some of this stuff like the movement logic but still be able to affect it later from our player script we could do this in the player script. Here we’re extracting that data while looking for that data, and creating some logic that will allow us to easily relocated that data.
public class Playerscript : Monobehavior
{
//we've migrated the speed values elsewhere to the movecontroller which we will initialze in awake
public float health;
public MoveController moveController; //You can set this by hand by dragging the gameobject itself onto this empty field
public float acceleration; //Maybe this is a powerup that lets the player gain speed over time that we want in this script for some reason
void Awake(){
moveController = GetComponent<MoveController>(); //SUPER IMPORTANT make sure that the movecontroller is on the same object as your player script. Just to be careful we can do a check and search children instead if for some reason the movement script ends up on a child object
if(!moveController) moveController = GetComponentInChildren<MoveController>(); //Now that we have the movement script initialized we can reference it elsewhere and modify public values or use public methods or whatever
if(!moveController) print("Unable to find MoveControllerScript in gameobject or children of " + name);
}
void FixedUpdate(){
if(moveController.rigidbody.velocity.magnitude < moveController.maxSpeed) moveController.moveSpeed += acceleration;
}
}
And here is what the movement script would look like that you would put on the same gameobject.
public class MoveController : Monobehavior
{
public float moveSpeed;
public float maxSpeed = 15f;
void FixedUpdate(){
MoveForward();
}
void MoveForward(){
transform.position.x += moveSpeed; //let's ignore that this doesn't actually work like this i'm just being lazy
}
}
It’s super important that you don’t do anything crazy in any awake script that will depend on other references, we want to do any sort of interconnected logic that requires multiple components to happen in the Start() area of our script to avoid broken links. So long as ALL your scripts adhere to this and you search for components properly in awake you should be fine.
If you end up having further issues with finding the right components at the right time you can create an independent script with a lower sorting order to initialize your dependencies on your assets.
This is the setup I largely use, this is what my gameobjects look like:
It may not be fancy and I kinda threw this all together kinda fast, but hope that helps.
Split via responsibility. But there’s nothing wrong with these components referencing one another. Dependencies are fine, though try and keep them one-way.
Also consider ways to decouple these components from one another if the dependencies are getting tangled up. Such as via delegates, or interfaces, as very often other components will just need to know when something important happens from one or more other components.
You’ll also find that most of these components don’t have any dependencies at all.
This is also a case of a backwards dependency. An ‘input script’ can/should just simply read input, and expose the necessary properties or methods for other components to use. Then the actual movement component can read these exposed properties and apply them to the player as needed.
You might even separate the component that does the actual movement into a separate component as well, so that other systems can interface with the player’s movement, without necessarily getting tangled up with the code that interprets input.
In the end it’s about designing an API for yourself and other systems in your project to use. Have clearly named properties, methods, delegates, etc, that allow parts of your system to reuse other parts.
And it also just comes down to experience. The more you do this the easier it becomes. The player in my current project has about 15+ components, but this has made the project easier to manage as opposed to more difficult.
Yeah finding reference when the components aren’t on the same object is a big pain. Instances are a really good way to do this
public class InputManager : MonoBehaviour
{
public static InputManager Instance; //This is a singular entity of a script that you can access from anywhere without having to initialize the reference between scripts.
public bool buttonAConditions => Input.GetMouseButton(0);
private void Awake()
{
Instance = this;
}
}
Now if you want to reference a property from this script all you need is a single gameobject in the scene with this script on it. Any more than that and they’ll fight with one another. You can put the below logic in any script and you can now access the data from the static Instance of the script.
public InputManager inputManager => InputManager.Instance;
var buttonA= inputManager.buttonAConditions ; //You can easily reference this script using the above reference or if you want to just grab the value from anywhere you can do the following
var buttonA= inputManager.Instance.buttonAConditions ; //will work anywhere in any script without need for grabbing the component, this is dependent on the other script itself initializing the static instance in the Awake method
Keep in mind that this will only work at runtime after your instance script has time to initiate itself at Awake
If you need multiple instances of data from these singular managers you can start getting into arrays.
Not really what I meant. The implication was that rather than the input component reading input and driving the player, the input component just reads input and allows other components to interpret these inputs. This doesn’t really matter whether they are on the same game object or not.
public sealed class InputModule : MonoBehaviour
{
private float _xInput;
private float _yInput;
public float XInput => _xInput;
public float YInput => _yInput;
private void Update()
{
_xInput = Input.GetAxis("Horizontal");
_yInput = Input.GetAxis("Vertical");
}
}
public sealed class MovementModule : MonoBehaviour
{
[SerializeField]
private InputModule _inputModule;
[SerializeField]
private Rigidbody _rigidbody;
private void Update()
{
float xInput = _inputModule.XInput;
float yInput = _inputModule.YInput;
Vector3 direction = new(xInput, 0, y);
Vector3 position = _rigidbody.position;
Vector3 nextPosition = position + direction;
_rigidbody.MovePosition(nextPosition);
}
}
And ideally we don’t go promoting really basic, dangerous singleton implementations like that. Ideally we use as little singletons as we can. (Also why is something named ‘Input’ handling scoring?).
Ok my bad, I was just looking for an excuse to talk about Static Instances and how it’s so easy to access their data while including everyone in the convo
It’s called the Singleton Pattern. And while convenient there is a lot of considerations and downsides one needs to bear in mind. Such as object lifetime, order of initialisation, among other things.
An an input component and a movement component are no doubt either going to be on the same game object, or the same heirarchy, so referencing can simply done via the inspector. I don’t thing the Singleton pattern deserves a mention here.
Yeah i just found out about 'em not long ago. But i found the simplicity and ease of use amazing and I use 'em all over now. It’s liberating to be able to offload bits of data elsewhere and not have to load everything up in one place or always worry about references.
Are there performance concerns regarding accessing data from these Singletons we should know about?
The concern isn’t performance, it’s scalability. Once a project grows large enough, it becomes a problem to manage a growing number of singletons, particularly as they grow interdependent. It becomes difficult to test anything; when something breaks, everything breaks; and so on.
I recently had to rip out a number of singletons from my current project.
In any case, this is offtopic with respect to OP’s topic.
I work the same way purely for speed when prototyping.
If I don’t have any pain from a large multi-responsibility script, I generally leave it until I do have pain from it, then I refactor.
When refactoring something larger and hairier, it can he helpful to use partial class files as a stepping stone towards breaking unrelated stuff apart.
Partial classes in Unity3D:
Just to add my 2 cents, kind of unrelated but I have seen it in a few posts recently. The Instance = this
in Awake
is like looking for trouble. It can lead to some hard to find bugs because of race conditions.
In Unity, Awake
runs before OnEnable
for the same script, but not all Awake
finish running before starting the OnEnable
, for example if we have scripts A and B, the order can be A.Awake -> A.OnEnable -> B.Awake -> B.OnEnable
.
When something is initialized in Awake
, like the instance
, if a script tries to use it from OnEnable
, it may run ok or you may get a null reference exception, like in the previous example if we initialized the instance in B.Awake
but we tried to use it from A.OnEnable
.
What is bad about this bug, is that it may run ok a hundred times and then suddenly appear. It depends on the order Unity decides to run the scripts. Even if it works ok in editor or in a build, it may break after a Unity upgrade or a new build.
In general, don’t use other game object’s fields in OnEnable
that are getting initialized in that object’s Awake
method. If you absolutely have to, then do this:
Make a boolean with false default value in the second script that if it is true in OnEnable
then you use the field, then use the field in the Start
method and there make the boolean true, so that when the object is created the field is used from the Start
method and after that, for every Disable/Enable, it is used from OnEnable
.
Because this is cumbersome to remember and do for everything that is using a singleton, make the instance equal to this in a property to save yourself some pain.
Good to know. I’ve run into all kinds of initialization problems. Eventually I created an initialization script that I set to fire before all other scripts and that script finds all the components and assigns the proper reference all at once, rather than relying on all the other scripts trying to find each other on awake.
I always wondered why this was such a headache and this might explain some of it.
And while there are best practices and absolute right and wrong ways to do things, it’s fine for people do things that are mostly ok but not totally optimal because they learn along the way.
If everyone was just forced to do everything the perfect way all the time without learning the hard way we’d all be locked in doing things without truly understanding why they are doing things a certain way, people have to explore and experiment over a long period of time to truly become competent. And a lot of this stuff is a bit subjective.
Thanks everyone ^^ , I’ll test each of the ideas and see what is better for me.
So I finally did the split, kinda in the expected way, I created other classes that have different things to do, now I have the PlayerMovement, Player, PlayerInput, PlayerAim.
But instead of taking the references directly I used scriptableObjects to be the “middle man”.
I was already using the scriptableObj to make other objects - Like the GameManager or the zombie - get the player transform and the player class
In the player class
[SerializeField] private PlayerAttributes playerAttributes; //The ScriptableObj ref
private void Awake()
{
playerAttributes.value = transform;
playerAttributes.playerMovement = this;
}
In the zombie class
[SerializeField] private PlayerAttributes playerAttributes; //The ScriptableObj ref
public void Start()
{
playerTransform = playerAttributes.value;
}
Now that I divide the work on different classes I started to use the same scriptableObj to get the references to the others classes without needing to reference directly
all the classes have
[SerializeField] private PlayerAttributes playerAttributes; //The ScriptableObj ref
private void Awake()
{
playerAttributes.playerAim = this;
}
//Only for the classes that need reference, not every class need reference as Spiney's said
private void Start()
{
playerInput = playerAttributes.playerInput;
}
I’m being careful not to have a problem similar to meredoth’s warning. I guess it’s safe because the scriptableObj have all the values in the awake and only in the start other class start to get the references. What u guys think?
If it works it works…
This line kinda confuses me:
Shouldn’t this be named “playerAttributes.transformAsset” or something similar? Seems confusing to name a transform “value”
Also do these scripts share the same transform? If so you could skip this reference altogether and just use “transform” or “transform.root” if it’s the root transform (the top most parent), I believe
Sorry for the late answer, I didnt see the reply.
The name value is only because I was following a tutorial, and the guy at the tutorial just put value as a sample name to whatever you wanted to save there, I just never changed the name XD.
thats true, I can just skip the reference for the components inside my player object, but this transform is important to the zombies prefabs get it, without the need to find the player transform in the scene. Anyway thanks!
so long as the movement script has a reference to the main player script you could have very easy access to the transform with some logic like:
private transform keyTransform => mainPlayerScript.transform;
Then just use “keyTransform” instead of “transform” or whatever you want to call it.
But if they’re scripts on children of the zombi you shoudl be able to just use transform.root assuming they aren’t childed for any reason
If you make your player an instance or singleton, anytime you need to access its transform you can just do player.Instance.transform from anywhere. In fact in most my scripts I make a reference to the player
public PlayerScript player => PlayerScript.Instance;
And then if I wanted the transform it’s as easy as player.transform
yeah, making the player a singleton is a possibility, but Im planning on using netcode for gameObjects so Its seems more safe to work with scriptableObjects.