What is the best way to encapsulate inherited fields in Unity 2D? - c#

I'm currently designing a Unity game, and I want to ensure I'm implementing proper encapsulation. However, I'm stuck on this problem:
Suppose I have a class Item with the field: private string itemName. I also have a class Strawberry that is a subclass of Item. I want to write a method in class Strawberry that returns its itemName.
These scripts are MonoBehaviour, so they cannot contain constructors that would solve the problem above using base in its constructor.
What strategy should I use to set the Strawberry class' name?
Thanks!

You need to make your itemName string protected. That means it can be accessed by child classes.
Item base class:
using UnityEngine;
public class Item : MonoBehaviour
{
protected string itemName = "itemBase";
}
Strawberry class:
using UnityEngine;
public class Strawberry : Item
{
// Start is called before the first frame update
void Start()
{
Debug.Log($"my mane is: {GetItemName()}");
}
string GetItemName() => this.itemName;
}

Related

How to create a MonoBehaviour fabric for non-MonoBehaviour classes?

I am developing a small nuclear reactor simulator game. I have a bunch of reactor component classes: HeatVent, HeatExchanger, UraniumCell etc. They are not deriving from MonoBehaviour since they don't have any Unity logic, but they do implement a shared interface IReactorComponent. What I want to do is to be able to create prefabs of such components (simple heat vent, advanced heat vent, doubled uranium cell etc.) The prefabs would have different sprites and something like that, but the main issue is to define what reactor component class the prefab is related to, because I can't just drag'n'drop a non-MonoBehaviour script on inspector. Also, I want to be able to set settings in the inspector (for example, HeatVent has CoolAmount and HeatCapacity properties, UraniumCell has FuelAmount, HeatProduce and PowerProduce properties).
I have read about factory method pattern and as I understood, I have to create a fabric class that derives from MonoBehaviour for each reactor component class like HeatVentBehaviour, HeatExchangerBehaviour etc. Yes, that completely solves my issue with prefabs but is there any way to not create an additional MonoBehaviour wrap for each class? If I had 15 IReactorComponent classes, I would need to create 15 fabrics which feels like not the greatest solution.
Sounds like what you are looking for is ScriptableObject!
Instances of those are assets so they don't live in a scene but in the Assets folder and basically behave a little bit like prefabs except: They already exist and do not need to be instantiated anymore.
Mostly they are used as just configurable data containers. They have an Inspector so you can easily fill them with your desired data and references to other assets (e.g. the related prefab in your case).
But in addition you can as well let them implement behavior like your interface and thereby change the behavior of your scene objects by using different implementations of a method from different ScriptableObjects!
For the factory you then only need to figure out for which method to use which ScriptableObject instance e.g. either by having different methods or by having a Dictionary where you fill in your SO references.
Just as an example how this might look like (make sure each MonoBehaviour and ScriptableObject has its individual script file with matching name)
SpawnManager.cs
public class SpawnManager : MonoBehaviour
{
[SerializeField] private ReactorComponentBehaviour _behaviourPrefab;
[SerializeField] private BaseReactorComponent[] _components;
public bool TrySpawn<T>(out T component, out ReactorComponentBehaviour componentBehaviour) where T : IReactorComponent
{
component = default(T);
componentBehaviour = default;
var foundComponent = components.FirstOrDefault(c => c.GetType() == typeof(T));
if(foundComponent == null)
{
Debug.LogError($"No component found of type {T.GetType().Name}!");
return false;
}
// Here Instantiate doesn't spawn anything into the scene but
// rather creates a copy of the ScriptableObject asset
// This is just to avoid that any changes in the fields during the game
// would change the original ScriptableObject asset and thereby ALL related behavior instances
component = Instantiate ( (T) foundComponent);
// This now indeed spawns the related MonoBehaviour + GameOver
componentBehaviour = Instantiate (behaviourPrefab);
componentBehaviour.Init(component);
return true;
}
}
BaseReactorComponent.cs
public abstract class BaseReactorComponent : ScriptableObject, IReactorComponent
{
public abstract void WhateverIReactorComponentNeeds();
// Common fields and methods e.g.
public Sprite Icon;
}
HeatVent.cs
[CreateAssetMenu]
public class HeatVent : BaseReactorComponent
{
public int CoolAmount;
public int HeatCapacity;
public override void WhateverIReactorComponentNeeds ()
{
// Do something
}
}
UraniumCell.cs
[CreateAssetMenu]
public class UraniumCell : BaseReactorComponent
{
public int FuelAmount;
public int HeatProduce;
public int PowerProduce;
public override void WhateverIReactorComponentNeeds ()
{
// Do something
}
}
And finally you need only one base prefab with the
ReactorComponentBehavior.cs
public class ReactorComponentBehavior : MonoBehaviour
{
[SerializeField] private Image _image;
private IReactorComponent _component;
public void Init(IReactorComponent component)
{
_componemt = component;
// Do other stuff like e.g. adjust visuals according to the component etc
_image.sprite = component.Icon;
}
// And then use whatever this behavior should do with the assigned component
}
So in the end you would use that like e.g.
if(spawManagerReference.TrySpawn<HeatVent>(out var component, out var componentBehaviour)
{
// Do something with the behavior e.g. set its position, parent etc
}
else
{
Debug.LogError($"Failed to get a {nameof(HeatVent)}!");
}
If then at some point you still want different additional behaviours you could let them inherit from the common ReactorComponentBehavior and rather reference the prefabs inside the BaseReactorComponent itself .. then every component can bring its own prefab but still have a common core behaviour

Can a MonoBehaviour be a private inner class?

I want to use a private inner class MonoBehaviour as part of the internal implementation of a class (also a MonoBehaviour). Reduced to the essentials:
public class OuterClass : MonoBehaviour {
public void SomeMethod(GameObject go) {
go.AddComponent<InnerClass>();
}
private class InnerClass : MonoBehaviour {
}
}
This appears to be all working absolutely fine, with no errors or warnings. But it also appears to violate the statement in the Unity documentation that:
The class name and file name must be the same to enable the script
component to be attached to a GameObject.
Is this a problematic violation of that rule? If so, what are the problems? I am aware that it means the script is not visible in the Editor, but that's exactly what I want: it should never be added by anything other than its enclosing class.

how to configurate nested object in Unity's inspector

I tried to create nested object in C# script in Unity and change the values in it.
Class of nested object
using UnityEngine;
public class Replica : ScriptableObject
{
public string Text;
public int Speed = 1;
public int PersonaID = 0;
}
Class with nested object
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
[CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/DialogContainer", order = 1)]
public class DialogContainer : ScriptableObject {
public Replica[] Replicas; // Here is nested object
public Sprite[] Avatars;
}
And when I created the ScriptableObject I saw that:
Photo of ScriptableObject interface
Here I can only put an instance of the class there, but I cannot configure it.
But i want to change values right in inspector without creating and inserting object of class "Replica" like in InputManager where I can create one more obj in axis, open it and change values in inspector like that.
Photo of interface I want to see
If u want configurate your class in inspector, that class must be not derived from MonoBehaviour or ScriptableObject and be with tag [System.Serializable] than u can serialize it in nested class without creating instance.
Replica:
[System.Serializable]
public class Replica
{
public string Text;
public int Speed = 1;
public int PersonaID = 0;
}
Dialog Container:
using System.Collections.Generic;
using UnityEngine;
public class DialogContainer : MonoBehaviour{
public List<Replica> Replicas;
public Sprite[] Avatars;
}
Than I saw that:Inspector screenshot
Unless you make a custom editor for DialogContainer, you can't do what you want to do. If you have a parent ScriptableObject that contains a child list of ScriptableObjects, you can't edit each child's fields when you have the parent selected in your Project View.
You will have to edit each child Replica by selecting the Replica instance in the Project View. Then the Inspector will let you edit that Replica.

Creating a list of <T> class on an object

This is a common question and I assure you I have done my research first. I simply cannot get a list of all of the instances of a script type on the game object.
I have tried making an array of the types and looping the contents into a list. This gives me conversion errors.
I have tried directly adding the array to the list with .AddRange. Conversion errors.
I have tried the different formats of GetComponents, and casting the output of the array into every applicable type I can think of, with no success.
I have also tried initising the list first and then running GetComponent in start.
I have tried using CharEquipGenre as both monobehaviour and non-monobehaviour.
What am I doing wrong?
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
public class CharEquipment : MonoBehaviour
{
public List<CharEquipGenre> equipment_genres = GetComponents <CharEquipGenre>(); // I am trying to do something like this
public CharEquipGenre attack;
public CharEquipGenre defend;
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CharEquipGenre
{
public List<BlockScriptableObject> equipped = new List<BlockScriptableObject>();
}
// Additional code via request:
public class CharEquipment : MonoBehaviour
{
void Start()
{
equipment_genres = GetComponents<CharEquipGenre>();
}
public List<CharEquipGenre> equipment_genres = new System.Collections.Generic.List<CharEquipGenre>();
public CharEquipGenre attack;
public CharEquipGenre defend;
Well, CharEquipGenre is not a MonoBehaviour, so there are no "components" of this class.
You can find all components of a specific type with GetComponents (read more here) but it needs to be a MonoBehaviour that's attached to the GameObject
I don't know unity3d but it seems as though you are trying to initialize equipment_genres when defining it and for that the compiler would need access to something that is available at compile time.
If GetComponents is a method on the class then this will not work as the method is not static. You could use the instance by perhaps going with a method or expression body:
public List<CharEquipGenre> equipment_genres => new List<CharEquipGenre>(GetComponents<CharEquipGenre>());
Although a slightly "better" design would be exposing IEnumerable:
public IEnumerable<CharEquipGenre> EquipmentGenres => ...
// or
public IEnumerable<CharEquipGenre> GetEquipmentGenres() => ...
First of all make your class [Serializable] so you can see it in the inspector and save it.
[Serializable]
public class CharEquipGenre
{
public List<BlockScriptableObject> equipped = new List<BlockScriptableObject>();
}
And then simply instantiate a new list like
public List<CharEquipGenre> equipment_genres = new List<CharEquipGenre>();
Note that both of those will be overruled by whatever you do to them in the Inspector later. Once you added elements any change to the initialization lines is useless (unless you hit Reset in the MonoBehaviour's context menu)

Can I use an abstract base class as a Unity Editor element?

I'm trying to create a component for a Unity GameObject, let's call it MediaController. I want it to be able to manage timing (play/pause/etc) for different media (audio/video). I created an abstract class PlayableMedia with basic properties/fields/methods and created 2 classes, PlayableVideo and PlayableAudio, that inherit and implement according to our needs.
The intent was to have a singular list of PlayableMedia that could be audio/video agnostic, allowing an easy (i.e.) media.Play() call regardless of type at specific app times... but my field public List<PlayableMedia> MediaList; is not appearing in the editor and there is no error.
So ultimately my question is as the title states: is it possible to use the PlayableMedia class as the type of a field?
I'm suspecting "no" based on my experiences with this, but I've found links that say "yes" or "yes, sort of" that seem to point to custom editors/inspectors/drawers, but I have 0 experience with those and haven't been able to get it implemented (see below).
[System.Serializable]
public class RegisteredMedia
{
public float StartTime;
public PlayableMedia Media;
}
[CustomPropertyDrawer(typeof(RegisteredMedia))]
class RegisteredMediaDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), new GUIContent("Playable Media"));
var indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
Rect rectStartTime = new Rect(position.x, position.y, 30, position.height);
Rect rectMedia = new Rect(position.x + 35, position.y, 50, position.height);
EditorGUI.PropertyField(rectStartTime, property.FindPropertyRelative("StartTime"), GUIContent.none);
EditorGUI.PropertyField(rectMedia, property.FindPropertyRelative("Media"), GUIContent.none);
EditorGUI.indentLevel = indent;
EditorGUI.EndProperty();
}
}
public class MediaController : MonoBehaviour
{
public List<RegisteredMedia> MediaList = new List<RegisteredMedia>();
\\[...] rest of implementation
}
Can anyone help me out? Either confirm that it isn't possible, or help me with an implementation if it is?
Also, if it can be done with custom editors/inspectors/drawers, can someone help me get a single item in the List<RegisteredMedia> to display as Start Time ____ Playable Media [=====] (where PlayableMedia will be a GameObject with the proper component attached)?
Be careful of your use of the word "property". In C# it means something very specific.
is it possible to use the PlayableMedia class as the type of a property?
I think you are asking the wrong question here. Rather than coming up with a new implementation, consider why your current implementation might not be working?
Firstly, I'll give you the following example:
public abstract class Car : MonoBehaviour { }
public class Jeep : Car { }
public class Ferrari : Car { }
public class CarHolder : MonoBehaviour
{
public List<Car> Cars;
}
In this example, I could create a GameObject with the CarHolder component, and was able to attach both Jeep and Ferrari Objects. It is important to note that each monoBehavior class I defined sits in its own file and the file name matches the class name. This is just how Unity works.
So to answer the question I think you are asking (assuming we replace "property" with "field"), it is indeed possible to use abstract class types and have them show up in the inspector. I suspect that you need to separate your classes into separate files.
It's possible natively since 2019.3 release via [SerializeReference] attribute https://docs.unity3d.com/ScriptReference/SerializeReference.html
e.g.
using System.Collections.Generic;
using UnityEngine;
using System;
[Serializable]
public abstract class AbstractExample {
public int foo;
}
// [Serializable] not needed here
public class ConcreteExample : AbstractExample {
}
public class Consumer : MonoBehaviour {
[SerializeReference]
public List<AbstractExample> examples = new() { new ConcreteExample() };
// both the list and the concrete instance visible in the editor
// and editable without any additional editor extensions
// note that you can't effectively add new items to the list via editor
// since that way you create a faulty abstract-ish instances instead
// (no actual way for the editor to know what subtype do you want)
// if you're OK with having the base class being not explicitly abstract
// and can provide a sensible constructor for it, just drop the abstract
// you'll still have full polymorphism support etc. with SerializeReference
}

Categories