In my game the objective is to pop balloons which will increase the score, however I am now reconsidering how I design the connection between the scripts. I realise that having the balloon script directly communicate to the score script could lead to spaghetti code in the future which makes the project hard to maintain and extend. Even if I used events so the scripts simply listened to each other, they still need to know each others existence. I feel as though the balloon script should be responsible for any tasks to do with the balloon and the score script should be just for the score. Does anyone have any recommendations for connecting scripts?
You don’t have to connect scripts per say. You just need pass around references of the instances you want to deal with.
If you want to get those references once and store a static reference for easy referencing (or to turn specific objects into singletons) it would potentially be less problematic than Ermiqs answer.
I think what you are conflating is how code is written/ designed and referential code.
The references aren’t the prohibiting/ causal factor in your code being poorly written/ designed.
Maybe instead of looking there you could try learning some design patterns?
If it’s about reducing the size of classes and keeping things organised maybe use composition. Maybe move all of your methods into a service and dependency inject them into the class.
Apart from that there a many ways to deal with event handling in c# events are a native language feature
There is also support in the framework for the observer pattern
If you are working with a lot of observables it may be worth investigating RX, although if you are asking these sorts of questions it may be a little bit dense.
One method is to use interfaces to describe methods that are used to pass information between the Score Handlers and the Score Givers. This will make the handlers and other scripts dependent on the interface and not on each other. We could create a basic interface like below.
public interface IScoreHandler {
void AddScore(int amount);
}
Then any Score Handler would have to implement that interface. Then you could potentially make different Score Handler, then you could swap out in different scenes. Allowing you for change of behaviors without having to update all scripts. Below is an example of a basic Score Handler.
public class ScoreHandler : MonoBehaviour, IScoreHandler {
public int Score { get; private set; }
public void AddScore(int amount) {
this.Score += amount;
}
}
Next we need to get access to the Score Handler in any script that requires it. A simple method would be to use the built in GameObject Tags. By using tags we can set the correct tag on any Score Handler you wish to use and we’ll be able to find it in the scene. Then we can add a simple utility function to find it from any script that requires it, see below.
public class ScoreUtility {
public const string ScoreHandlerTag = "ScoreHandler";
public static IScoreHandler GetScoreHandler() {
var gameObject = GameObject.FindObjectWithTag(ScoreHandlerTag);
if (gameObject != null) {
// Find the component that implements the interface
return gameObject.GetComponent<IScoreHandler>();
} else {
// Could not find score handler in scene
return null;
}
}
}
Now any GameObject that requires a Score Handler can call this method to get a reference. Below you can find an example usage.
public class ScoreGiver: MonoBehaviour {
// Score to give
public int scoreAmount;
// Reference to the current score handler
private IScoreHandler scoreHandler;
public void Awake() {
// Get the current score handler in the scene,
// and store the reference for later use.
this.scoreHandler = ScoreUtility.GetScoreHandler();
}
public void GiveScore() {
// Give the score, if there is a handler
if (this.scoreHandler != null) {
this.scoreHandler.AddScore(this.scoreAmount);
}
}
}
Disclamer: The following is a newbie answer. Not the best advice obviously, but that’s what I would like to hear if I’ve been asking the same question here. I actually asked quite similar question here on Unity Answers, and I’ve got a good answer but the answer was lacking of practical examples.
Well, since balloons and score tracker are related to each other, i.e., the score script have to know about what is happening to balloons and a balloon need to notify the score tracker somehow, it’s not that bad if you have them connected via references. There’s no way to get rid of references completely anyway.
However, does a score tracker really need to know about every balloon out there? I mean, the score tracker doesn’t have to store a reference to every balloon, it just needs some receiver that could receive a message whenever some of the balloons is popped, but the score tracker could not care which balloon it was actually, it could be something like OnSomeBallonPopped(int scoresEarned) { score += scoresEarned; }
, or even more abstract: when some balloon is popped, the public method of the score tracker should be called AddScore(int scoresToAdd) { score += scoresToAdd }
.
I see two options here:
Option 1: Score tracker as a static class.
Since there’s usually just one single score tracker instance in a game (there’s just one player and he has one score tracker), you could use a static score tracker class, and any balloon just needs to call the ScoreTracker.AddScore(int scoresEarned);
when the balloon is popped. And the score script don’t need to know about balloons at all.
Example:
public static class ScoreTracker {
private static int score = 0;
public static void AddScore(int amount) {
score += amount;
}
}
public class Balloon {
private int rewardScore = 5;
void OnPopped() {
ScoreTracker.AddScore(rewardScore);
}
}
Later, when you decide to add some particle effects to be played on the popped balloon position, you’ll probably need to add one more line within the OnPopped()
method:
public class Balloon {
private int rewardScore = 5;
void OnPopped() {
ScoreTracker.AddScore(rewardScore);
MyParticleEffectsManager.PlaySomeExplosionAt(this.transform.position);
}
}
So, you’ll have to add new connections anyway.
Option 2: Static events.
Kind of similar to the option 1 but more advanced and utilises delegates. I myself have learned it from this video: How & why to use Static Events in Unity3D - YouTube That man is a real professional and he has a lot of useful tutorials, tips and tricks about Unity and game development in general.
So, basically, what you need is: a) implement a static event within a balloon class to fire it whenever a balloon is popped; b) subscribe for this event in the score tracker class. So far it sounds like a usual events, but the difference between usual events and static events is: you don’t subscribe to every separate balloon instance but to the balloon class event as a whole.
The events are usefull when you don’t want to inform the balloon about other classes out there, you just fire the event in the balloon class and let the event listeners to do there job somewhere else.
But in this case you’ll need to send the whole balloon instance instead of just its rewardScores or position, because you don’t know which property the listener will need actually.
Example:
using System; //this is needed to use C# delegates
public class Balloon {
public static event Action<Balloon> OnAnyBalloonPopped = delegate { }; //just like a usual event but it's static
private int rewardScore = 5;
public int RewardScores => rewardScores; //to provide the score amount to any listener from the outside
void OnPopped() {
//whenever the balloon is popped,
//notify any listener about it
OnAnyBalloonPopped(this);
}
}
In the score tracker you subscribe to the balloon pop event and decide how much scores to add for that particular balloon:
public class ScoreTracker : Monobehaviour {
private int score = 0;
void Awake() {
//subscribe to the event that is fired whenever any balloon is popped
Balloon.OnAnyBalloonPopped += AddScore;
}
public void AddScore(Balloon balloon) {
//find out how much scores to add for the balloon
score += balloon.RewardScores;
}
}
In particle effect manager you subscribe to the pop event as well, and get the position from the balloon:
public class MyParticleManager : Monobehaviour {
void Awake() {
Balloon.OnAnyBalloonPopped += PlayBalloonExplosion;
}
public void PlayBalloonExplosion(Balloon balloon) {
Vector3 pos = balloon.transform.position;
// instantiate an explosion at the 'pos'
}
}
So, as you can see, you need references between the classes and their instances anyway. But you can decide which class should know about the others:
- in option 1 the
Balloon
have to know aboutScoreTracker
andMyParticleManager
while they don’t know about theBalloon
; - in option 2 the
Balloon
doesn’t know who’s going to receive its event, but bothScoreTracker
andMyParticleManager
need to know about theBalloon
to do the stuff related to balloons.
Kind of like that. Hope it could be helpful somehow.