How to create a dynamic variable system with Scriptable Objects - c#

In this talk I learned how to create variables with scriptable objects, creating classes like FloatVariable, DoubleVariable, StringVariable and others. But, in the same talk, the guy said that he uses a more dynamic variable system, that prevents creating several classes to handle all variable types.
Using the first system, I had a C# script called ImageFillSetter, that given two float variables and a Image script, it returns the division of the two variables to the fillAmount variable of the image.
But, when I get a Double Variable, and I'd like to set a progress bar with this value, I need to create another script called ImageFillSetterDouble, and put in these variables. And if I needed to create one with Integers? Every time I create a script like this, I will need to create two duplicates to handle the other number variable types?
With this dynamic variable system, this problem should be solved, but I have no idea how to start/create this system.
The code looks like this:
[CreateAssetMenu(menuName="Variable/Float")]
public class FloatVariable : ScriptableObject, ISerializationCallbackReceiver
{
public float initialValue;
[NonSerialized]
public float value;
public void OnAfterDeserialize()
{
value = initialValue;
}
public void OnBeforeSerialize() { }
}
What I want is something like this (Totally hypothetical, I know that this doesn't works)
[CreateAssetMenu(menuName="Variable")]
public class Variable : ScriptableObject, ISerializationCallbackReceiver
{
public var initialValue;
[NonSerialized]
public var value;
public void OnAfterDeserialize()
{
value = initialValue;
}
public void OnBeforeSerialize() { }
}

I know there is an accepted answer that works, but I feel that the usage of ScriptableObject variables as described in the linked video was misinterpreted.
I think you would be better off making your FloatVariable independent of the calculation.
Let's say the calculation is for player health and your fill value would be calculated by currentHealth/maxHealth.
public class PlayerHealth: MonoBehaviour
{
[SerializeField] private FloatVariable floatReference;
[SerializeField] private float maxHealth;
[SerializeField] private float currentHealth;
void Update()
{
this.floatReference.value = currentHealth/maxHealth;
}
}
public class ImageFillSetter: MonoBehaviour
{
[SerializeField] private FloatVariable floatReference;
[SerializeField] private Image imageReference;
void Update()
{
this.imageReference.fill = this.floatReference.value;
}
}
Or let's say that player health is stored as double:
public class PlayerHealth: MonoBehaviour
{
[SerializeField] private FloatVariable floatReference;
[SerializeField] private double maxHealth;
[SerializeField] private double currentHealth;
void Update()
{
this.floatReference.value = (float)(currentHealth/maxHealth);
}
}
Now let's say that you add an input field where the fill value can be entered as a percentage string (like '76'):
public class FillInput: MonoBehaviour
{
[SerializeField] private FloatVariable floatReference;
[SerializeField] private Input input;
void Update()
{
if(Input.GetKeyDown(KeyCode.Enter))
{
this.floatReference.value = float.Parse(input.text)/100f;
}
}
}
The ImageFillSetter will 'observe' the FloatVariable without being aware of how that float was calculated.
This way you only ever have to have one ImageFillSetter that can be used for any image and any data source, while having 1 or more ways of altering the fill that does not require any changes to be made to ImageFillSetter.
For example, let's say that you want to use the same approach to indicate async level load progress:
public class FillInput: MonoBehaviour
{
[SerializeField] private FloatVariable floatReference;
private AsyncOperation loadOperation;
void LoadLevelAsync(string levelName)
{
this.loadOperation = SceneManager.LoadLevelAsync(levelName, LoadSceneMode.Additive);
}
void Update()
{
this.floatReference.value = this.loadOperation?.progress ?? 0;
}
}
This will work without making any other changes as long as your ImageFillSetter references the same FloatVariable.
Think of the FloatVariable (or whichever primitive you have eg. DoubleVariable) as a value stored in a database. Anyone can read the value and anyone can save a new value. It would be strange to store all possible calculations for the value in the database instead of doing the calculation and just storing the answer.
This does not change the fact that you need Scriptable implementations for each primitive:
FloatVariable
DoubleVariable
StringVariable
BoolVariable
etc
but you will only need one of each as demonstrated in the first section of derHugo's answer.

Have a look at Generics
Have one abstract class like
public abstract class ValueAsset<T> : ScriptableObject
{
public T value;
// Add your methods
// Here some more examples also using the T value. They might also be abstract but they don't have to be
// return a T
public T GetValue()
{
return value;
}
// or pass a T
public void SetValue(T input)
{
value = input;
}
}
This class you will never instantiate but now derive multiple implementations from it e.g.
[CreateAssetMenu(fileName = "new int", menuName = "ValueAssets/int")]
public class IntValue : ValueAsset<int>
{
// Maybe constructors here or additional fields and methods
}
[CreateAssetMenu(fileName = "new float", menuName = "ValueAssets/float")]
public class FloatValue : ValueAsset<float>
{
// Maybe constructors here or additional fields
}
You can also have multiple generic values like
public abstract class OtherExample<TKey, TValue> : ScriptableObject
{
// Note that this is just an example
// Dictionary is not serializable
public Dictionary<TKey, TValue> values = new Dictionary<TKey, TValue>();
public void AddPair(TKey key, TVakue value)
{
values.Add(key, value);
}
}
And implement something like
public OneImplementation : OtherExample<string, GameObject>
{
//...
}
The same way this can be used for reference values (components, GameObject etc)
So for IntValue the method GetValue will return an int and SetValue will take an int as parameter. The same way they take and return a float in FloatValue.
Doing the same thing with an ImageFillSetter<T> you can than make your method abstract and implement different behaviours for different T values (like e.g. a different parsing etc)
Note: I don't know why exactly but in the past I noticed that
public ValueAsset<T> valueAsset;
will not be serialized in the inspector even if later implemented so you have to implement the field with the correct type in the implementation instead. You also still could override it on runtime but you can skip the whole FetchValue part if you don't need it and anyway use valueReference instead - just added it for completeness.
public abstract class ImageFillSettet<T> : MonoBehaviour
{
// Will not appear in the Inspector
public ValueAsset<T> ValueAsset;
// Override this in implementation
protected abstract void FetchValue();
// Use it for Initializing the value
private void Awake ()
{
FetchValue();
}
public abstract void SetFill();
}
Than later
public class ImageFillSetterFloat : ImageFillSetter<float>
{
// Show in the inspector
[SerializeField] private FloatValue valueReference;
// Provide the reference to the base class
protected override void Fetch value()
{
valueAsset = valueReference;
}
public override void SetFill()
{
// Use valueReference for something
}
}

Related

How do I add serialized field for a function in Unity?

I need to put a function from one class into another. And I want to put in the serialized field not the whole GameObject of the desired class, but one function from this class. How should I do it?
For example, I have a class GameActions describing game events.
public class GameActions : MonoBehaviour
{
public void actionStart()
{
SceneManager.LoadScene("Game");
}
}
And I want to be able to fire a function actionStart in another class by putting it into serialized field (inside of toggleEnd, for example).
public class Countdown : MonoBehaviour
{
[SerializeField] private SomeType toggleEnd;
[SerializeField] private float currTime;
}
To use the Unity Inspector to store and serialise classes, you would expose the class as a field. The Inspector will then let you drag and drop a class into the object field.
public class Countdown : MonoBehaviour
{
// A reference to the GameActions class is serialised here.
[SerializeField] private GameActions _gameActions;
[SerializeField] private float currTime;
// This is an example of a countdown timer.
private void Update ( )
{
currTime -= Time.deltaTime;
if ( currTime < 0 )
{
// This is how you call the GameActions actionStart method.
_gameActions.actionStart ( );
this.enabled = false;
}
}
}

Making static class data serializable

Consider the following:
public class myClass : MonoBehaviour
{
public int i; //<- value set from inspector
static myClass()
{
Awake();
}
private static void Awake()
{
Debug.Log(i); //<- Error CS0120
}
}
This would throw me an error error CS0120: An object reference is required for the non-static field, method, or property 'myClass.i'
I could then (according to this page) try to write it in another way:
public class myClass : MonoBehaviour
{
public int i;
static myClass()
{
Awake();
}
private static void Awake()
{
var mc = new myClass();
Debug.Log(mc.i); //<- NullReferenceException
}
}
But that would throw me error NullReferenceException: Object reference not set to an instance of an object myClass.Awake ().
Meaning that I cannot serialize static variables? Is there a workaround? I'm sorry but I'm still getting used to C# and if you could also give me a brief theoretical reason why it doesn't work it would help me greatly in understanding. Thanks!
First of all: Your issue has nothing to do with a field being serializable or not.
It is rather related to your instanced class field i not being accessible from a static context. As the error states you would need an instance of your class in order to access it there but
As also mentioned classes of type MonoBehaviour are not allowed to have any constructor and may not be instanciated via the new keyword in Unity. The only allowed ways of creating instances of components is via Instantiate, AddComponent or via the constructor of e.g. new GameObject("someName", typeof(YOUR_COMPONENT));.
You don't need a Singleton for what you want. It sounds like you actually would want to go this way round:
public class myClass : MonoBehaviour
{
// This one you set via the Inspector
[SerializeField] private int _i;
// This is now a read-only property
// That can only be set by this class
public static int i { get; private set; }
private void Awake()
{
// Your instance can always access its static fields
// So this way you can assign the value
i = _i;
}
}
In general we would need more input in order to figure out your actual usecase here. It is also possible that you could rather use an entirely static class like e.g.
public static class myClass
{
public static int i = 42;
}
this makes your field not serialized but simply accessible from everywhere without the need of an instance in the Scene. You would simply access it from another MonoBehaviour like e.g.
public class Example : MonoBehaviour
{
private void Start()
{
Debug.Log(myClass.i);
}
}
Or you might want to make your class not static at all but rather access it through the correct reference like
[Serializable]
public class myClass
{
public int i = 42;
}
public class Example : MonoBehaviour
{
// Since it is tagged Serializable and now serialized in the Inspector
// an instance is created for this field automatically
[SerializedField] private myclass _myClass;
private void Awake()
{
Debug.Log(_myclass.i);
}
}
Unity can only serialize a specific instance of an class (monobehaviour). Static information is shared between all instances of the class and thus cannot be serialized by unity.
Additionally, do not put a constructor in your monobehaviours, the object is constructed by the unity engine using its own process and the awake function is called automatically. You're getting that null reference exception because the awake function is being called from your constructor and not from unities internal functions that are supposed to be initializing it, so things that need to be set up for it haven't been.
You haven't said why you need static information serialized but if you're trying to use a singleton approach there are a couple simple methods for doing it in unity:
class MySingleton : MonoBehaviour
{
[SerializeField] private int someDataA;
[SerializeField] private int someDataB;
public static MySingleton Instance
{
get;
private set;
}
public int SomeDataA
{
get
{
return someDataA;
}
set
{
someDataA = value;
}
}
public int SomeDataB
{
get
{
return someDataB;
}
set
{
someDataB = value;
}
}
private void Awake()
{
Instance = this;
}
}
alternatively
class MySingleton : MonoBehaviour
{
[SerializeField] private int someDataA;
[SerializeField] private int someDataB;
private static MySingleton instance;
public static int SomeDataA
{
get
{
return instance.someDataA;
}
set
{
instance.someDataA = value;
}
}
public static int SomeDataB
{
get
{
return instance.someDataB;
}
set
{
instance.someDataB = value;
}
}
private void Awake()
{
instance = this;
}
}
In the first example you can access the current instance of the component by going MySingleton.Instance and access your properties and functions on it. In the second example the instance is kept private and all the properties and functions are made static so you can access them directly on MySingleton without needing to see the instance.

How can i re-use a code in monobehaviour class?

First i'd like to give a short version of my question:
How can i access another code pieces attached to another game object, or how can i initiazlie a class without have an game object attched.
When i making a small game in Unity, i made an unit designer where you give some value such as how many weapon does it carry, and the status of that unit (attack, range, speed, etc.) will be calculated by ComputeValues() and saved when you click confirm. But all those values were adjusted by clicking a button instead of direct input. (I.e. Click a button and add/reduce 1 weapon)
However, when i try to add some template unit at start up it won't work. So i made a CreateDesignWithValue() function. Which takes input for all the related data, and use the ComputeValues() above to compute the value for that object.
The problem is i'm trying to do it in player class. But i can't create new ShipDesigner, and neither can i set it to static. How can i get access to it?
Without knowing you exact usecase and what the methods do you are talking about we can only give a very general answer:
Not all classes have to be of type MonoBehaviour it really depends on your needs.
Extension Methods
If you have a certain calculation for a certain type you can use Extension Methods like
public static class Vector3Extensions
{
public static Vector3 DevideBy(this Vector3 a, Vector3 b)
{
return new Vector(a.x / b.x, a.y / b.y, a.z / b.z);
}
}
which you can use like e.g.
var newVector = transform.position.DevideBy(new Vector(1, 2, 3));
in all other classes.
public static class
In general you can use a public static class to implement methods and store values that shall be executable from everywhere e.g.
public static class Settings
{
private static int _currentInt = 7;
public static void SaySomething(string something)
{
Debug.Log(something);
}
public static void DoubleCurrentInt()
{
_currentInt *= 2;
}
public static int GetSquareOfCurrentInt()
{
return _currentInt * _currentInt;
}
}
which you can call now from everywhere like
Settings.DoubleCurrentInt();
Settings.SaySomething(Settings.GetSquareOfCurrentInt.Tostring);
Instances
Ofcourse sometimes you do not want that something is accessible from everywhere so you can also simply have a normal instanced class for your calculation like
public class Settings
{
private int _currentInt = 7;
public Settings(int initialInt = 0)
{
_currentInt = initialInt;
}
public void SaySomething(string something)
{
Debug.Log(something);
}
public void DoubleCurrentInt()
{
CurrentInt *= 2;
}
public int GetSquareOfCurrentInt()
{
return CurrentInt * CurrentInt;
}
}
So you can use
private Settings settings;
private void Start()
{
new Settings(3);
}
in one MonoBehaviour and
private Settings settings;
private void Start()
{
new Settings(26);
}
in another MonoBehaviour, both have different instances but can use all the implemention in it for calculating and doing stuff individually.
public static void
you can also only "share" one method among all instances of a certain type (static) and also allow other types to access it (public)
public class A : MonoBehaviour
{
// A prefab only this specific component has access to
[SerializeField] private GameObject prefab;
// example for a kind of singleton pattern
private static GameObject prefabSingleton;
private void Start()
{
prefabSingleton = prefab;
}
public static void Spawn(int someIntToAssign, string someTextToAssign)
{
var obj = Instantiate(prefabSingleton)
;
componentReference = obj.GetComponent();
componentReference.someIntField = someIntToAssign;
componentReference.Getcomponent<Text>().text = someTextToAssign;
}
}
this you can call from other types as well like
A.Setup(someExampleReference, "Yeay!");
(in this example you could consider to rather implement it in SomeExampleType, though ^^)
ScriptableObjects
What you described also sounded like ScriptableObjects (Tutorial) might be interesting for you.
ScriptableObjects are kind of assets similar to prefabs but can store values and also methods. You than can reference them in fields of MonoBehaviour components to change their behaviour according to the values or in order to share it as kind of container between multiple instances and different types.
Instance with public method
Last but not least the most "usual" of doing it would be to have a
public class A : MonoBehaviour
{
[SerializeField] private Transform someObject;
public Vector3 GetObjectPosition()
{
return someObject.position;
}
}
and access it via one of the many GetComponent or/and FindObjectOfType variants or simply by referencing the according component like
public class B : MonoBehaviour
{
// drag in via the Inspector
public A AReference;
private void Start()
{
// or get it on runtime e.g.
AReference = GameObject.Find("ObjectWithA").GetComponent<A>();
// or if there is only one e.g.
AReference = FindObjectOfType<A>();
Debug.Log(AReference.GetObjectPosition());
}
}
Answer of short versions:
How can i access another code pieces attached to another game object:
Declare a public field for the script you want to reach e.g. public ExampleScript exampleScript; and assign the gameobject which has ExampleScript to your field in the inspector.
how can i initiazlie a class without have an game object attched: You can't create an instance of a script derived from MonoBehaviour just like new ExampleScript();. But instead you can add that script to your existing gameobject with gameObject.AddComponent<ExampleScript>(); and you can reach this script from another script which is attached the very same gameObject like: gameObject.GetComponent<ExampleScript>();

Proper way to create a static virtual factory method in C#

I'm implementing classes for Effects (something with a duration that applies a behavior in the FixedUpdate loop while it is active) in Unity3D.
I have a base abstract Effect class which has the behavior for keeping track of the duration, removing itself when the duration is up, and calling a protected abstract _doEffect function while its duration is up. In my derived classes, I override _doEffect to create Effects with different behaviors.
public abstract class Effect : MonoBehaviour
{
public virtual float kDuration { get { return 1.0f; }}
public static bool IsStackable { get { return false; }}
private float _elapsed = 0.0f;
protected virtual void Start()
{
_elapsed = kDuration;
}
protected virtual void FixedUpdate()
{
_elapsed -= Time.fixedDeltaTime;
if(_elapsed <= 0) {
Destroy(this);
}
_doEffect();
}
protected abstract void _doEffect();
}
Now, because you can't use constructors with Unity3D, I need a way to do the following for each derived Effect class when I'm applying a new Effect of that type to a game object:
1) If this type of effect is not stackable, then remove all other instances of this monobehaviour from the game object.
2) Create a new component of the effect type to the game object.
3) Do some initialization specific to that effect type.
For these requirements, I was imagining doing something like
public class DerivedEffect : Effect
{
public override float kDuration { get {return 1.0f; }}
public static bool IsStackable { get { return true; }}
private int _derivedData;
public static void Create(GameObject obj, int data)
{
DerivedEffect effect = DerivedEffect.CreateEffect(obj);
effect._data = data;
}
protected override void _doEffect()
{
//Do some stuff
}
}
and then in the base class putting
public static virtual Effect CreateEffect(GameObject obj)
{
//T is somehow magically the type of the class you called this function on
if(!T.IsStackable()) {
//delete all components of type T on obj
}
T effect = obj.AddComponent<T>();
return effect;
}
Obviously this isn't possible unless I do some weird stuff with generics and reflection that seems a bit extreme and probably not that right way to do things.
The crux is that I want a static function that does 1), 2), 3), and I want to share the code that does 1) and 2), and 1) depends on a bool which is different for every derived class.
What is a proper, working design for these desiderata?
What is a proper, working design for these desiderata?
Unity is component based and gets things complicated when you want to use it the way you in a normal C# application.
The simplest way is to use Composition. Make the Effect class it's own class that is not abstract. Just a normal class that inherits from MonoBehaviour. You can easily create new instance of it with AddComponent and get it with GetComponent. This script can also destroy itself directly after the timer is done counting without any problems.
Create a global variable in the DerivedEffect class to hold the instance of the Effect script that is created and this can be re-used over and over again until it becomes null which means that the script is destroyed. Note that there is no inheritance involved here and DerivedEffect script is only used as an example of the script that manages the Effect script.

Dynamic Variables, hard to explain in title, sorry

I'm wondering how I could get a String Variable (CurrentWeapon), to work like a Class reference (not sure how I can explain, as I am quite the noob, as you probably understand) to get different class variables from different classes depending on what weapon I have selected. The following script is a class where I have stored all the properties and stats for a weapon.
public class Stats_SMG : MonoBehaviour {
public static string Name = "SMG";
public static int Damage = 20;
public static float FireRate = 0.3f;
public static int Magazine = 25;
public static int ReserveMagazines = 2;
}
The following script is how I try to "dynamically" access my weapons stat-classes (assume that I have multiple of the scripts above with different names and properties to differnet weapons, etc)
public class Shooting : MonoBehaviour {
Stats_SMG smg;
Stats_AssaultRifle ar;
public string CurrentWeapon;
void Start() {
CurrentWeapon = smg;
Debug.Log (CurrentWeapon.Damage);
}
The above is just a quick sample of what I have to (hopefully) explain to you the problem that I have. Sorry if any sentences or words are misspelled or weird, as English is not my first language.
What I'd do instead is that my weapon will have assigned script with stats to simply do:
public class Shooting : MonoBehaviour {
public Weapon currentWeapon;
void Start() {
currentWeapon = smg; // Get somehow the currentWeapon object
WeaponStats stats = currentWeapon.GetComponent<WeaponStats>();
Debug.Log (stats.Damage);
}

Categories

Resources