The main issue is, I'm trying to use a polymorphic list in a ScriptableObject but its contents are lost when restarting Unity; code below.
So I've got a base class:
[Serializable]
public class BaseAction : ScriptableObject
{
public virtual void OnGUI()
{
}
public virtual void ExecuteAction()
{
}
}
And an inheriting class with a simple string attribute:
[Serializable]
public class DebugLogAction : BaseAction
{
[SerializeField]
private string debugText = default;
public override void OnGUI()
{
debugText = EditorGUILayout.TextField(debugText);
}
public override void ExecuteAction()
{
Debug.Log(debugText);
}
}
They all reunite in a ScriptableObject that contains a list of BaseActions:
[Serializable]
[CreateAssetMenu(fileName = "NewActionContainer", menuName = "ScriptableObjects/Action Container")]
public class ActionContainer : ScriptableObject
{
[SerializeField]
private List<BaseAction> actions = default;
public void OnEnable()
{
if (actions == null)
{
actions = new List<BaseAction>();
}
}
public void OnInspectorGUI()
{
foreach (var action in actions)
{
action.OnGUI();
}
if (GUILayout.Button("Add Debug Log Action"))
{
actions.Add(CreateInstance<DebugLogAction>());
}
}
}
I've written up a custom editor for the ActionContainer ScriptableObject which works fine, displaying the button for adding multiple actions which adds editable input fields:
[CustomEditor(typeof(ActionContainer)), CanEditMultipleObjects]
public class CustomActionContainerInspector : Editor
{
private ActionContainer actionContainer;
public void OnEnable()
{
actionContainer = (ActionContainer)target;
}
public override void OnInspectorGUI()
{
actionContainer.OnInspectorGUI();
}
}
However, on restart of Unity, the content of the ScriptableObject is gone (assembly rebuilding is fine, playing/stopping the game keeps the data as well).
I've tried adding a field to BaseAction to check if the values are just sheared off the inheriting class, but no; the entire list of actions from the ActionContainer empties.
I believe there's an issue either with serializing or deserializing the elements from the list itself, since it's polymorphic, but I've got no idea how to debug or solve the issue; internet searches didn't get me to a solution either.
First of all in general you should not call your method OnGUI as this is also a built-in message in MonoBehaviour and might lead to confusion.
Your main pitfall I think is the dirty state.
If you don't mark objects dirty after changing them then your changes don't get saved correctly.
I think a quick fix for this would be to use EditorUtility.SetDirty
public override void OnGUI()
{
EditorGUI.BeginChangeCheck();
debugText = EditorGUILayout.TextField(debugText);
if(EditorGUI.EndChangeCheck())
{
EditorUtility.SetDirty(this);
}
}
or if you wan to properly support undo/redo then rather using Undo.RecordObject
public override void OnGUI()
{
EditorGUI.BeginChangeCheck();
var newDebugText = EditorGUILayout.TextField(debugText);
if(EditorGUI.EndChangeCheck())
{
Undo.RecordObject(this, "changed debug text");
debugText = newDebugText;
}
}
See also
EditorGUI.BeginChangeCheck / EditorGUI.EndChangeCheck
And similar also for the
if (GUILayout.Button("Add Debug Log Action"))
{
//either
Undo.RecordObject(this, "add element");
actions.Add(CreateInstance<DebugLogAction>());
//or
//EditorUtilty.SetDirty(this);
}
Related
I am trying to use virtual and abstract methods to make my game architecture better.
I'm using C# and Unity for this example.
I use a ShipComponent as a base Class because I want all the child classes to do the same thing.
But sometimes I want a certain ShipComponent to do something else.
The code will make it a lot clearer:
ShipComponent.cs:
public abstract class ShipComponent : MonoBehaviour
{
[HideInInspector] public ShipControl shipControl;
public virtual void Init(ShipControl control)
{
this.shipControl = control;
}
public virtual void IsPlayer()
{
SetListeners();
}
public abstract void IsNotPlayer();
public abstract void ReEnable();
public abstract void SetListeners();
}
One of the many child classes that inherits from ShipComponent:
public class Rudder : ShipComponent
{
[Header("Settings")]
public Transform rudder;
[Header("Debug Info")]
[SerializeField] float rudderSpeed;
[SerializeField][Range(-45, 45)] int setRudderAngle = 0;
[SerializeField][Range(-45f, 45f)] float realRudderAngle = 0f;
public override void Init(ShipControl shipControl)
{
base.Init(shipControl);
rudder = transform.GetChild(0).GetChild(4);
StartCoroutine(SmoothRudderChange());
SetListeners();
}
public override void IsPlayer()
{
base.IsPlayer();
}
public override void IsNotPlayer()
{
PlayerShipControl.OnRudderChange -= SetRudder;
}
public override void ReEnable()
{
StartCoroutine(SmoothRudderChange());
SetListeners();
}
public override void SetListeners()
{
PlayerShipControl.OnRudderChange -= SetRudder;
if (!shipControl.shipWrapper.ship.IsPlayer) return;
PlayerShipControl.OnRudderChange += SetRudder;
}
void OnDisable()
{
PlayerShipControl.OnRudderChange -= SetRudder;
StopAllCoroutines();
}
The main draw back I experience with this, is that I have to copy paste all 5 or 6 methods everytime I create a new ShipComponent class.
It seems messy and theres a lot of repeating code, most of the time the only difference in each ShipComponent is the SetListeners part, and StartCoroutines if any.
Is there a way to dynamically set delegate listeners up?
So I could set them in the base class ShipComponent?
Instead of setting each component individually?
Another script that inherits from ShipComponent for completeness:
public class Guns : ShipComponent
{
IEnumerator mouseAimCycle;
public override void Init(ShipControl shipControl)
{
base.Init(shipControl);
InitCannons();
SetListeners();
}
public override void ReEnable()
{
SetListeners();
}
public override void IsPlayer()
{
base.IsPlayer();
mouseAimCycle = AimCycle();
StartCoroutine(mouseAimCycle);
SetListeners();
}
public override void SetListeners()
{
PlayerShipControl.OnFireGuns -= TryFire;
if (!shipControl.shipWrapper.ship.IsPlayer) return;
PlayerShipControl.OnFireGuns += TryFire;
}
public override void IsNotPlayer()
{
StopCoroutine(mouseAimCycle);
PlayerShipControl.OnFireGuns -= TryFire;
}
void OnDisable()
{
PlayerShipControl.OnFireGuns -= TryFire;
StopAllCoroutines();
}
Calling the ShipComponent virtual and abstract methods:
public class ShipControl : MonoBehaviour
{
// Contains Ship + Cargo + Crew and a ref to this ShipControl
public ShipWrapper shipWrapper { get; private set; }
ShipComponent[] shipComponents;
// Gather all ShipComponents and Initialize them.
public void Start()
{
shipComponents = transform.GetComponents<ShipComponent>();
foreach (ShipComponent comp in shipComponents)
{
comp.Init(this);
}
}
// Call this to check if this is players current ship and set the components accordingly.
public void UpdateIsPlayer()
{
if (!shipWrapper.ship.IsPlayer)
foreach (ShipComponent component in shipComponents)
component.IsNotPlayer();
else
foreach (ShipComponent component in shipComponents)
component.IsPlayer();
}
And PlayerShipControl, which I use for input, broadcasting the input through delegates, and the theory is that only the players currently controlled ship will be listening for this input:
public class PlayerShipControl : MonoBehaviour
{
public static event Action<Transform> SetCamToPlayerShip;
public static event Action SetShipPanelUI;
public static event Action<bool> ToggleAnchorIcon, ToggleFlagIcon, ToggleAutofireIcon, ToggleBoatsIcon;
public static event Action OnFireGuns;
public static event Action<int> OnRudderChange;
public static event Action<int> OnSailStateChange;
public static event Action<bool> OnAllAnchorsCommand;
public static event Action<bool> OnAllBoatsCommand;
bool anchor, flag, autofire, boats;
ShipControl shipControl;
void Update()
{
if (Input.GetKeyUp(KeyCode.W)) // Raise Sails SailState++
{
OnSailStateChange?.Invoke(1);
}
if (Input.GetKeyUp(KeyCode.S)) // Furl Sails SailState--
{
OnSailStateChange?.Invoke(-1);
}
if (Input.GetKey(KeyCode.D))
{
OnRudderChange?.Invoke(1);
}
if (Input.GetKey(KeyCode.A))
{
OnRudderChange?.Invoke(-1);
}
if (Input.GetKeyDown(KeyCode.M))
{
OnRudderChange?.Invoke(0);
}
// Drop All Anchors
if (Input.GetKeyDown(KeyCode.V))
{
anchor = true;
ToggleAnchorIcon?.Invoke(anchor);
OnAllAnchorsCommand?.Invoke(anchor);
}
// Haul All Anchors
if (Input.GetKeyDown(KeyCode.H))
{
anchor = false;
ToggleAnchorIcon?.Invoke(anchor);
OnAllAnchorsCommand?.Invoke(anchor);
}
// Drop All Boats
if (Input.GetKeyDown(KeyCode.B))
{
boats = true;
ToggleBoatsIcon?.Invoke(boats);
OnAllBoatsCommand?.Invoke(boats);
}
// Take In All Boats
if (Input.GetKeyDown(KeyCode.U))
{
OnAllBoatsCommand?.Invoke(false);
// TO DO When all boats are back on deck, boatIcon + boatsBoolFlag should be turned off again.
}
if (Input.GetKeyDown(KeyCode.Space))
{
OnFireGuns?.Invoke();
}
}
}
Its a long string of scripts sometimes though so I have left out all the managers and such.
Ship ship inside shipWrapper.ship is a custom data class that stores the info about the ship, not a Monobehaviour, but it holds a bool called IsPlayer aswell. Nothing else of interest I can think of.
The main draw back I experience with this, is that I have to copy paste all 5 or 6 methods every time I create a new ShipComponent class. It seems messy and there's a lot of repeating code, most of the time the only difference in each ShipComponent is the SetListeners part, and StartCoroutines if any.
In the show example you have more differences between implementations then ones described. Without seeing the full code it is hard to suggest something meaningful.
Few notes on the current code:
In Rudder you don't need to specify IsPlayer because the following:
public override void IsPlayer()
{
base.IsPlayer();
}
does not add anything extra, so you can just skip implementation in the derived class.
Based on provided examples it seems that ReEnable can be defined as virtual in base class with default implementation set to calling SetListeners (the same approach as you have with Init and IsPlayer).
PlayerShipControl.Update possibly can be improved by moving handlers to dictionary. Something along this lines:
public class PlayerShipControl : MonoBehaviour
{
// ...
Dictionary<KeyCode, Action> keyActions = new() // not sure about the type
{
{ KeyCode.W, () => OnSailStateChange?.Invoke(1) },
// ...
{ KeyCode.V, () =>
{
anchor = true;
ToggleAnchorIcon?.Invoke(anchor);
OnAllAnchorsCommand?.Invoke(anchor);
}
},
// ...
};
void Update()
{
foreach (var kvp in keyActions)
{
if (Input.GetKeyUp(kvp.Key))
{
kvp.Value();
break;
}
}
}
}
The following is attached to my player and would call upon whatever object is hit to use the objects function. Think the player controls the pointing and clicking, but the object controls whatever the object will do, such as turn on a light.
void Interact()
{
RaycastHit interactablehit;
if (Physics.Raycast(PlayerCamera.transform.position, PlayerCamera.transform.forward, out interactablehit, MaxDistance))
{
// if raycast hits, then it checks if it hit an object with the tag Interactable.
if (interactablehit.transform.tag == "Interactable")
{
object = interactablehit.transform.name;
interactablehit.transform.gameObject.GetComponent<interactablehit.transform.name>().ObjectInteract();
}
}
}
This is a job for either an interface or a common base class:
public interface IInteractable
{
void ObjectInteract();
}
and then have your different implementations like e.g.
public class ExampleInteractable : MonoBevahiour, IInteractable
{
public void ObjectInteract()
{
Debug.Log("Hello!");
}
}
or
public class ToggleLightInteractable : MonoBevahiour, IInteractable
{
[SerializeField] private Light light;
public void ObjectInteract()
{
light.enabled = !light.enabled;
}
}
and then simply do
void Interact()
{
if (Physics.Raycast(PlayerCamera.transform.position, PlayerCamera.transform.forward, out var interactablehit, MaxDistance))
{
var interactable = interactablehit.transform.GetComponent<IInteractable>();
if (interactable != null)
{
interactable.ObjctInteract();
}
}
}
Or the same with a common base class, usefull if you want/need some shared behaviour or default implementations
public class BaseInteractable
{
public virtual void ObjectInteract()
{
Debug.Log("Hello!");
// For demo reasons lets say per default this can be interacted with only once
Destroy(this);
}
}
and then have your different implementations like e.g.
public class ExampleInteractable : BaseInteractable
{
public override void ObjectInteract()
{
// completely override the default behaviour
Debug.Log("Hello World!");
// this can be interacted with forever since the default behavior is not executed
}
}
or
public class ToggleLightInteractable : BaseInteractable
{
[SerializeField] private Light light;
public override void ObjectInteract()
{
// keep the default behavior and only extend it with something additional
base.ObjectInteract();
light.enabled = !light.enabled;
}
}
and then do
void Interact()
{
if (Physics.Raycast(PlayerCamera.transform.position, PlayerCamera.transform.forward, out var interactablehit, MaxDistance))
{
if (interactablehit.transform.TryGetComponent<BaseInteractable>(out var interactable))
{
interactable.ObjctInteract();
}
}
}
If you really really for what reason ever (there shouldn't be any good one) need to stick to string you can (you shouldn't) use SendMessage like
void Interact()
{
if (Physics.Raycast(PlayerCamera.transform.position, PlayerCamera.transform.forward, out var interactablehit, MaxDistance))
{
interactablehit.transform.gameObject.SendMessage(interactablehit.transform.name, options: SendMessageOptions.DontRequireReceiver);
}
}
which would require your object to be called exactly like the method you want to call, regardless of how the component is called.
I have a Unity + Zenject setup with a ProjectInstaller with some global dependencies that adhere to a "modal" interface, e.g.,
public class ProjectInstaller : MonoInstaller {
public override void InstallBindings() {
Container.Bind<ModalManager>().AsSingle();
Container.Bind<Modal>().To<DialogManager>().AsSingle();
}
}
Some modals are only relevant to certain scenes, so I bind those in the SceneInstaller:
public class SceneInstaller : MonoInstaller {
public override void InstallBindings() {
Container.BindInterfacesAndSelfTo<InventoryManager>()
.FromComponentInNewPrefab(InventoryPrefab)
.AsSingle()
}
}
I want to manage all modals from the single ModalManager, defined at the project scope. So it has a List<Modal> binding:
public class ModalManager : MonoBehaviour {
[Inject]
protected List<Modal> _modals;
}
When I run this, the ModalManager only gets a single modal: the one defined in the project scope. In my understanding the SceneContext is a subcontainer of the ProjectContext. So I should be able to use FromSubContainerResolve in the ProjectInstaller to bind items in the child scene, perhaps by adding a line like:
// ProjectInstaller.cs
public override void InstallBindings() {
// ...
Container.Bind<Modal>().To<InventoryManager>().FromSubContainerResolve();
}
But I'm not sure which of the eleventy FromSubContainerResolve methods make sense for this case. They all seem pertinent to prefabs with a game object context, not for use from within the ProjectContext.
Does this use case make sense? Is there an easier or better way?
The problem is that that ModalManager can only be injected with dependencies that are added directly to ProjectContext. For these kinds of problems I recommend using the following pattern:
public interface IModal
{
}
public class ModalManager
{
private readonly List<IModal> _modals = new List<IModal>();
public IReadOnlyList<IModal> Modals
{
get { return _modals; }
}
public void AddModal(IModal modal)
{
_modals.Add(modal);
}
public bool RemoveModal(IModal modal)
{
return _modals.Remove(modal);
}
}
public class ModalRegisterHandler : IInitializable, IDisposable
{
private readonly List<IModal> _modals;
private readonly ModalManager _modalManager;
public ModalRegisterHandler(
// We need to use InjectSources.Local here, otherwise we will
// add any project context modals again in each scene
[Inject(Source = InjectSources.Local)]
List<IModal> modals, ModalManager modalManager)
{
_modals = modals;
_modalManager = modalManager;
}
public void Initialize()
{
foreach (var modal in _modals)
{
_modalManager.AddModal(modal);
}
}
public void Dispose()
{
// We don't want ModalManager to retain references to Modals defined in unloaded scenes
// (dispose is executed on scene unload)
foreach (var modal in _modals)
{
_modalManager.RemoveModal(modal);
}
}
}
public class SceneInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container.Bind<IModal>().To<FooModal>();
Container.Bind<IModal>().To<BarModal>();
}
}
public class ProjectInstaller : MonoInstaller
{
public override void InstallBindings()
{
// We use CopyIntoDirectSubContainers so that ModalRegisterHandler gets automatically added to every
// scene context
Container.BindInterfacesTo<ModalRegisterHandler>().AsSingle().CopyIntoDirectSubContainers();
Container.Bind<ModalManager>().AsSingle();
Container.Bind<IModal>().To<QuxModal>();
Container.Bind<IModal>().To<FizzModal>();
}
}
I would like to combine ScriptableObject along with UnityEvent and GenericObject usage. My ultimate goal is to create generic event and listener and then use ScriptableObject to create specific events e.g. GameObject, int and etc. and handle these with respective listeners.
Here is the code I have so far:
EventTemplate.cs
using System.Collections.Generic;
using UnityEngine;
public class EventTemplate<T> : ScriptableObject {
private List<ListenerTemplate<T>> listeners = new List<ListenerTemplate<T>>();
public void Raise(T go) {
for (int i = listeners.Count - 1; i >= 0; i--) {
listeners[i].OnEventRaised(go);
}
}
public void RegisterListener(ListenerTemplate<T> listener) {
listeners.Add(listener);
}
public void UnregisterListener(ListenerTemplate<T> listener) {
listeners.Remove(listener);
}
}
ListenerTemplate.cs
using UnityEngine;
using UnityEngine.Events;
[System.Serializable]
public class ResponseEvent<T> : UnityEvent<T> { }
public class ListenerTemplate<T> : MonoBehaviour {
//[SerializeField]
public EventTemplate<T> gameEvent;
//[SerializeField]
public ResponseEvent<T> response;
private void OnEnable() {
gameEvent.RegisterListener(this);
}
private void OnDisable() {
gameEvent.UnregisterListener(this);
}
public void OnEventRaised(T go) {
response.Invoke(go);
}
}
Now, when I have both generic types, I created one Event and one Listener for int type.
These are two files:
EventInt.cs
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "New Event Template", menuName = "Stage Management/Event Templates/Event Int")]
public class EventInt : EventTemplate<int> {
}
and ListenerInt.cs
using UnityEngine;
using UnityEngine.Events;
[System.Serializable]
public class ResponseInt : ResponseEvent<int> { }
public class ListenerInt : ListenerTemplate<int> {
}
then my expectation was, once I add ListenerInt.cs to specific game component via Editor, I will able to access gameEvent and response in the same fashion I can access them as if I define UnityEvent for int type.
However, the reality is that I cannot see / access neither gameEvent nor response via the Editor.
Unity serialization doesn't work on generics T.
you would need to explicitely create an inherited non-generic type for everything you want to serialize in the Inspector. You would need e.g. a
[Serializable] public class IntEvent : UnityEvent<T> { }
in order to be able to serialize it.
In order to do what you want (kind of) I would do this:
First use an interface like
public interface IEventListener<in T>
{
void OnEventRaised(T value);
}
Then make your ListenerTemplate
public abstract class ListenerTemplate<T> : MonoBehaviour, IEventListener<T>
{
// These have to be provided by the inheritor
public abstract UnityEvent<T> unityEvent { get; }
public abstract EventTemplate<T> gameEvent { get; }
private void OnEnable()
{
gameEvent.RegisterListener(this);
}
private void OnDisable()
{
gameEvent.UnregisterListener(this);
}
public void OnEventRaised(T value)
{
unityEvent.Invoke(value);
}
}
As you can see any class inheriting from ListenerTemplate<T> will have to somehow provide both the UnityEvent<T> and the EventTemplate<T>.
So e.g.
// The specific scriptable object doesn't change it just inherits
[CreateAssetMenu(fileName = "New Event Template", menuName = "Stage Management/Event Templates/Event Int")]
public class EventInt : EventTemplate<int>{ }
and
// Specific override for the UnityEvent
[Serializable] public class IntUnityEvent : UnityEvent<int> { }
public class ListenerInt : ListenerTemplate<int>
{
[SerializeField] private EventInt eventInt;
[SerializeField] private IntUnityEvent intUnityEvent;
// override and populate the two abstract properties
// with the references from the serialized fields
public override UnityEvent<int> unityEvent => intUnityEvent;
public override EventTemplate<int> gameEvent => eventInt;
}
This at least reduces the implementation overhead to these two fields for every inheritor and according specific implementations of EventTemplate and UnityEvent.
Finally the EventTemplate<T> just has to use a list of IEventListener instead
public abstract class EventTemplate<TValue> : ScriptableObject
{
private readonly List<IEventListener<TValue>> listeners = new List<IEventListener<TValue>>();
public void Raise(TValue go)
{
// actually why iterate backwards?
for (int i = listeners.Count - 1; i >= 0; i--)
{
listeners[i].OnEventRaised(go);
}
}
public void RegisterListener(ListenerTemplate<TValue> listener)
{
listeners.Add(listener);
}
public void UnregisterListener(ListenerTemplate<TValue> listener)
{
listeners.Remove(listener);
}
}
Hello i have this code:
public abstract class Test : MonoBehaviour
{
public abstract void OnStart();
public abstract void OnUpdate();
private void Start()
{
OnStart();
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.X))
{
Destroy(this);
}
OnUpdate()
}
}
then I have the this:
public class SecondTest : Test
{
public override void OnStart()
{
}
public override void OnUpdate()
{
Writter.Log("Running");
}
}
However whenever I press X, my writter keeps logging, it seems like the instance of "Test" gets destroyed but not my "SecondTest" is there any way to solve this? thank you!
*OP explained the intention is to cause Children to be destroyed when Parent is.
The solution would probably be better handled at a '(Test)Manager' or 'LevelManager'.
You could assign your Children or prefabs to your (Test)Manager manually, or get (Test)Manager to instantiate the child prefab.
GameObject child = Instantiate(prefab);
child.transform.SetParent(gameObject.transform); //set the child's parent to the Manager
Then in your (Test)Manager
private void Update()
{
if (Input.GetKeyDown(KeyCode.X))
{
for(int i=0;i<gameObject.transform.childCount;i++)
Destroy(gameObject.transform.GetChild(i).gameObject);
}
}
This is just a crude suggestion, there are probably better ways to do this such as holding some references to the children in the manager versus parenting.