Data safety in Scriptable Object based architecture - c#

Immediately I would like to say that if a similar question has been asked before, please point me to it.
Let's say I have a Scriptable Object that I use to pass a player's health from one system to another.
public class HealthSO: ScriptableObject {
[ReadOnly]
public float health;
}
A class called PlayerHealth sets the value in the Scriptable Object so that other systems can use it. E.g: the player's health bar.
It's great because I can freely connect different systems without referencing them, but it is not without its problems, and there is one that concerns me the most.
How do I make sure that the only class that can change the health value in the Scriptable Object is PlayerHealth?
Or maybe it is something that I shouldn't worry about too much? Sure if it is only one person working on a project then there isn't too much to worry about. But what if this approach would be applied in a bigger project?
Thanks!

This might be a bit controversial, but so is using ScriptableObject for this in the first place ^^
Unfortunately Unity still doesn't really support serializing of interface type fields. But in this case there is only two different access levels - read and write.
So you could do something like
// Just going generic here as latest Unity versions finally support it
// and you have way less re-implementation of the same functionality
public abstract class ReadonlyValueSO<T> : ScriptableObject
{
[SerializeField]
[ReadOnly]
protected T _value;
public T Value
{
get => _value;
}
}
public abstract class WriteableValueSO<T> : ReadonlyValueSO<T>
{
public void Set(T value)
{
_value = value;
}
}
// Some constants could even be ReadonlyValueSO if you never want to write over them anyway
[CreateAssetMenu]
public class HealthSO : WriteableValueSO<float>
{
}
This way in your setter component you would use the writeable type and do e.g.
public class SomeSetter : MonoBehaviour
{
[SerializeField] WriteableValueSO<float> health;
private void Update()
{
health.Set(health.Value + .1f * Time.deltaTime);
}
}
while in the consumers you only give it the readable
public class Consumer : MonoBehaviour
{
[SerializeField] ReadonlyValueSO<float> health;
private void Update()
{
Debug.Log(health.Value);
}
}
This way you have full control over who can read and who can write.
Another huge advantage: This way you also don't have to poll check values how I did above. You can rather simply add an even to be invoked whenever the value is set:
public abstract class ReadonlyValueSO<T> : ScriptableObject
{
[SerializeField]
protected T _value;
public T Value
{
get => _value;
}
public abstract event Action<T> ValueChanged;
}
public abstract class WriteableValueSO<T> : ReadonlyValueSO<T>
{
public void Set(T value)
{
_value = value;
ValueChanged?.Invoke(_value);
}
public override event Action<T> ValueChanged;
}
now your consumer could rather look like e.g.
public class Consumer : MonoBehaviour
{
[SerializeField] ReadonlyValueSO<float> health;
private void Awake()
{
// subscribe to event
health.ValueChanged -= OnHealthChanged;
health.ValueChanged += OnHealthChanged;
// invoke now once with the current value
OnHealthChanged(health.Value);
}
private void OnDestroy()
{
// IMPORTANT: Unsubscribe!
health.ValueChanged -= OnHealthChanged;
}
// Always and only called whenever something sets the value
private void OnHealthChanged(float newHealth)
{
Debug.Log(newHealth);
}
}

Related

Trying to change BattleState globally

Hi I'm a completely new to coding and am trying to create a card game. I've watched some tutorials and tried to take things into my own hands but cant seem to figure out something. I currently have a BattleState set up;
public enum BattleState { START, PLAYERMAINPHASE, PLAYERBATTLEPHASE, PLAYERENCORESTEP, ENEMYTURN, WON, LOST }
and would like it so when i change the BattleState with a script, it changes it for every other script that references this BattleState. Sorry for the bad wording. Coding is rough :/
You can use interfaces, create an interface such as IBattleStateChanger and have a method on it
interface IBattleStateChanger{
void ChangeBattleState(YourClass.BattleState state);
}
Then on every script you want the value to change implement this interface as
ClassExample : IBattleStateChanger {}
This will then force you to create a method in the script to change the state
After that, whenever you want to change the value globally on the scripts where you implemented this interface, you can do a foreach loop finding each type of this interface such as
BattleState newState = BattleState.START;
foreach (var obj in FindObjectsOfType<IBattleStateChanger>){
obj.SetBattleState(newState);
}
You could use a static event and attach listeners/callbacks to it like e.g.
public enum BattleState
{
START, PLAYERMAINPHASE, PLAYERBATTLEPHASE, PLAYERENCORESTEP, ENEMYTURN, WON, LOST
}
public static class BattleStateMgr
{
private static BattleState _state;
public static BattleState State => _state;
public static event System.Action<BattleState> OnStateChange;
public static ChangeState(BattleState s)
{
_state = s;
OnStateChange?.Invoke(_state);
}
}
public class OtherScript : MonoBehaviour
{
private void Awake()
{
BattleStateMgr.OnStateChagne += OnBattleStateChange;
}
private void OnDestroy()
{
BattleStateMgr.OnStateChagne -= OnBattleStateChange;
}
private void OnBattleStateChange(BatlleState newState)
{
Debug.Log($"Changed Battle State to{newState}", this);
}
}
I believe you are confused about the scope of your variable. Each script you place an instance of the enum Battlestate, is a local version of that enum. If you want the reference to be global, you will need to have a central point where all scripts can grab this reference.
public class BattleManager : MonoBehaviour
{
private BattleState battleState;
// setter / getters
public BattleState GetBattleState(){return battleState; }
public void SetBattleState(BattleState state){ battleState = state; }
}
You are going to want to make a single script that holds the only reference to your enum Battlestate, then have your other scripts reference the variable.
public class OtherScript : MonoBehaviour
{
// assign this reference in the inspector
[SerializeField] private BattleManager bm = null;
private void YourFunction()
{
if(bm.GetBattleState() == BattleState.randomStateHere)
{
// run logic here
}
}
}
There are a number of ways to go about doing this, but the easiest would most likely be by declaring the variable static.
public class BattleManager : MonoBehaviour
{
private static BattleState battleState;
// setter / getters
public static BattleState GetBattleState(){return battleState; }
public static void SetBattleState(BattleState state){ battleState = state; }
}
public class OtherScript : MonoBehaviour
{
private void YourFunction()
{
if(BattleManager.GetBattleState() == BattleState.randomStateHere)
{
// run logic here
}
}
}
I do not know how many scripts you need to access this variable, but if it is only a handful, I would instead assign references to the script that holds the enum to each of the scripts that need it. I would avoid simply using static as it is the easy approach but creates what is called a code smell. The reason for this is OOP (object-oriented programming) by design should generally not have mutable global variables.
If you have a single instance of an object that manages all of your battle activity and a lot of scripts need to access it, you can look into the Singleton pattern. As you are new to programming, I would not implement this pattern until you understand the time and place to properly use it. You can also completely avoid using it by properly assigning the references you need in the inspector or by using a Object.FindObjectOfType in either Start or Awake.

When I'm trying to set a value for a base class from a child class (inheritance) it does not work (Unity C#)

I'm creating an enemy (from EnemyCreator1 class) with both EnemyMove1 and MarkusEnemy scripts (EnemyMove1 is a parent class to MarkusEnemy class). In EnemyCreator1 class I set value mainState of the script EnemyMove1 to "CHASE", but when I'm trying to access it from that class it says that mainState is "IDLE" (Please read my coments below because there are more explanations about what am I trying to achieve)
public class EnemyMove1 : MonoBehaviour
{
public enum mainStates { IDLE, CHASE }
public mainStates mainState;
void Update()
{
Debug.Log(mainState); //mainstate == IDLE, but should be CHASE
}
}
public class EnemyCreator1 : MonoBehaviour
{
[SerializeField] private GameObject enemyPrefab;
public void CreateEnemyAndSetItsStateToChase()
{
GameObject enemy = Instantiate(enemyPrefab);
enemy.GetComponent<EnemyMove1>().mainState = EnemyMove1.mainStates.CHASE;
}
}
public class MarkusEnemy : EnemyMove1
{
void Update()
{
EnemyMove enemyMoveScript = GetComponent<EnemyMove>();
Debug.Log(enemyMoveScript.mainState); //mainstate == CHASE
}
}
From the above code it looks like you are inheriting from a different base class EnemyMove, not EnemyMove1.
Thank you guys for helping me, after searching for the information about base classes I decided that it is impossible to access directly it's variables from another objects' scripts so I just simply call methods with variables as arguments (I put variables in round brackets of the method)

C# shared property & field inheritance

I've been trying to perfectly structure this project I'm working on in different classes while maximizing the benefits of inheritance. So far however, it's given me more headaches than benefits.
Consider this:
public class SuperClass : MonoBehaviour
{
[SerializeField] protected Camera _camera;
}
and this
public class SubClass : SuperClass
{
}
Both scripts are attached to different game objects in the scene.
The Camera is to be assigned by dragging it in the inspector
I tried this, and unity seemed to tell me that I had to assign the camera to the SuperClass game object AND to the subclass game object, which makes no sense to me.
How can I assign a camera to SuperClass.cs, which is then used and shared by all of its subclasses?
Thanks in advance!
shared by all of its subclasses
Shared by classes could can only be achieved by using "static" (static variable or singleton).
A workaround could be
public class SubClass :SuperClass
{
[SerializeField] Camera camera;
void Awake()
{
if(camera!=null)
{
_camera=camera;
}
}
// Start is called before the first frame update
void Start()
{
camera=_camera;
}
}
To further extend the solution, you could write a editor script or just get the camera from the code.
You need to create public static Camera property somewhere and reference it in your code, using property:
public static class StaticValues
{
public static Camera Camera {get; set;}
}
public class SuperClass : MonoBehaviour
{
[SerializeField] protected Camera _camera
{
get
{
return StaticValues.Camera;
}
set
{
StaticValues.Camera = value;
}
}
}
public class SubClass : SuperClass
{
}

Change a variable's accessibility during runtime

I wish to know if there is room for changing a variable's accessibility during runtime in Unity C#? The reason I want this is to hide unnecessary public variables when my script uses another state. I tried some dynamic variables, but I get errors that it can't be found.
I have that springDistance but I want to use it only when the state of the trapType is springOut.
Some code:
public class SpringTrap : MonoBehaviour
{
private Transform objectToMove;
// Use this for initialization
public enum TypeOfTrap
{
springOut
}
[Tooltip("Set the type of trap here to use.")]
public TypeOfTrap trapType = TypeOfTrap.springOut;
public float springDistance;
void Start ()
{
objectToMove = transform.FindChild("ObjectsToAnimate");
}
void OnTriggerEnter2D(Collider2D other)
{
if (other.gameObject.CompareTag("Player"))
{
//if the player has entered the danger zone
switch (trapType)
{
case TypeOfTrap.springOut:
break;
}
}
}
IEnumerator SpringOut(float springDistance)
{
float deltaDist = 0.0f;
while(deltaDist < springDistance)
{
objectToMove.position +=
}
}
}
Manipulation of the inspector is done with custom editor scripts. There you can display things, e.g. based on conditions.
Here is an example that does a very similar thing to the requested (displaying different things based on an enum field):
http://answers.unity3d.com/questions/417837/change-inspector-variables-depending-on-enum.html
An easier way to do what you are trying to achieve is to create an abstract trap class and then subclass different types of traps.
The abstract trap class handles collisions and then calls the abstract method Trigger.
public abstract class Trap : MonoBehaviour {
void OnTriggerEnter2D(Collider2D other)
{
if (other.gameObject.CompareTag("Player"))
Trigger(other.transform);
}
protected abstract void Trigger (Transform victim);
}
A spring trap can then be easily created by extending Trap and overriding the Trigger method.
public class SpringTrap : Trap {
public float SpringDistance;
protected override void Trigger (Transform victim) {
victim.position += Vector3.up * SpringDistance;
}
}
Now you don't have to worry about hiding irrelevant properties based on trap type because the only properties that will appear in the inspector are those you add to that variation of trap.

Get Component to the Script vs Instance of the Script itself

In order to get variable(s), function(s) in another class, I have known 2 ways of doing this. First, is to use Get Component to the Script that we want to get the variable(s), function(s) into. Second, is to use Instance of the Script itself.
So I have made the following code:
First case: Get Component to the Script itself
public class Manager : MonoBehaviour
{
private AnotherManager _anotherManager;
private void Awake()
{
_anotherManager = GameObject.Find("Managers").GetComponent<AnotherManager>();
}
private void Start()
{
_anotherManager.myIntVariable = 10;
_anotherManager.MyFunction();
}
}
public class AnotherManager : MonoBehaviour
{
public int myIntVariable;
public void MyFunction()
{
}
}
Second case: Use Instance of the Script itself
public class Manager : MonoBehaviour
{
private void Start()
{
AnotherManager.instance.myIntVariable = 10;
AnotherManager.instance.MyFunction();
}
}
public class AnotherManager : MonoBehaviour
{
public static AnotherManager instance;
public int myIntVariable;
private void Awake()
{
instance = this;
}
public void MyFunction()
{
}
}
My question is: Is there any difference between those cases? In terms of good practice of coding for programmer or performance or it is just a matter of programmer's perspective or whatever else?
Thanks
The second example is the what is known as the Singleton Pattern and should be used very sparingly.
I try to never use the first approach either where you find the gameobject and hope it exists.
You can expose a field for the Unity Inspector so that you can wire it up the same as you can expose any other variable
public AnotherManager AnotherManager;
Alternatively, if you hate using public all over the place like that, like me, you can also indicate to Unity that you wish to expose this variable in the inspector with the SerializeField attribute
[SerializeField]
private AnotherManager anotherManager;
With both of these methods, you can then drag an an object that has the AnotherManager component attached into the field in the inspector.
If instantiated objects need access to this, you will need to wire it up when it is instantiated.
If you need help attaching it in unity I can attach some screenshots.

Categories