Is there a way of mimicking MonoBehaviour copy semantics in ScriptableObjects?
Say I have a MonoBehaviour like so:
public class DummyClassBehaviour : MonoBehaviour {
public DummyClass DummyClassTest; //ScriptableObject
public DummyClassBehaviour DummyBehaviourTest; //Another DummyClassBehaviour
}
And a ScriptableObject:
public class DummyClass : ScriptableObject {
public string Text = "";
}
When I duplicate(CTRL+D) a GameObject w/ DummyClassBehaviour attached, 'DummyBehaviourTest' copies as you would expect: If it references a MonoBehaviour in the GameObject I'm copying, the copy mechanism updates the reference to the same MonoBehaviour type in the new GameObject. If it references a MonoBehaviour in another GameObject, that reference remains unchanged.
The ScriptableObject, on the other hand, always references the original. So I end up with N GameObject's all sharing the same ScriptableObject (DummyClass) from the original GameObject. I'm using ScriptableObjects to allow serialization of non-Monobehaviour data classes.
As far as I can tell, and please someone correct me if I'm wrong, you cannot modify the serialization behavior of a ScriptableObject to match that of a MonoBehaviour. Namely that it should update references if a duplicate is made.
Instead I opted for a less than optimal solution, but it works. My class is assigned a unique identifier that gets serialized like everything else. I use this ID in DummyBehaviour.Awake() to create a lookup table that I can then use to reassign my DummyClass.
I'm not going to accept my own answer because I don't feel it answers my original question fully, but it's related:
[System.Serializable]
public class DummyClass {
// Unique id is assigned by DummyBehaviour and is unique to the game object
// that DummyBehaviour is attached to.
public int UniqueID = -1;
public string Text = "";
// Override GetHashCode so Dictionary lookups
public override int GetHashCode(){
int hash = 17;
hash = hash * 31 + UniqueID;
return hash;
}
// override equality function, allows dictionary to do comparisons.
public override bool Equals(object obj)
{
if (object.ReferenceEquals(obj, null))return false;
DummyClass item = obj as DummyClass;
return item.UniqueID == this.UniqueID;
}
// Allow checks of the form 'if(dummyClass)'
public static implicit operator bool(DummyClass a)
{
if (object.ReferenceEquals(a, null)) return false;
return (a.UniqueID==-1)?false:true;
}
public static bool operator ==(DummyClass a, DummyClass b)
{
if (object.ReferenceEquals(a, null))
{
return object.ReferenceEquals(b, null);
}
return a.Equals(b);
}
public static bool operator !=(DummyClass a, DummyClass b)
{
if (object.ReferenceEquals(a, null))
{
return object.ReferenceEquals(b, null);
}
return !a.Equals(b);
}
}
And my MonoBehaviour:
[ExecuteInEditMode]
public class DummyBehaviour : MonoBehaviour {
public List<DummyClass> DummyClasses = new List<DummyClass>();
// reassign references based on uniqueid.
void Awake(){
Dictionary<DummyClass,DummyClass> dmap = new Dictionary<DummyClass,DummyClass>();
// iterate over all dummyclasses, reassign references.
for(int i = 0; i < DummyClasses.Count; i++){
DummyClass2 d = DummyClasses[i];
if(dmap.ContainsKey(d)){
DummyClasses[i] = dmap[d];
} else {
dmap[d] = d;
}
}
DummyClasses[0].Text = "All items same";
}
// helper function, for inspector contextmenu, to add more classes from Editor
[ContextMenu ("AddDummy")]
void AddDummy(){
if(DummyClasses.Count==0)DummyClasses.Add(new DummyClass{UniqueID = 1});
else {
// Every item after 0 points to zero, serialization will remove refs during deep copy.
DummyClasses.Add(DummyClasses[0]);
}
UnityEditor.EditorUtility.SetDirty(this);
}
}
Related
Ok while I was observing for memory leakage
I found out raw(?) class instance(not derived from UnityEngine.Object) were not releasing properly.
public partial class GCWMonitor
{
private static Dictionary<Type, int> _counter = new Dictionary<Type, int>();
public static int GetCount(Type type)
{
_counter.TryGetValue(type, out var count);
return count;
}
public GCWMonitor()
{
var instanceType = this.GetType();
if (_counter.ContainsKey(instanceType) == false)
_counter.Add(instanceType, 1);
else
_counter[instanceType]++;
//if (instanceType == typeof(IntervalTimerGate))
// DebugX.LogError(instanceType.Name, _counter[instanceType]);
}
~GCWMonitor()
{
var instanceType = this.GetType();
if (_counter.ContainsKey(instanceType) == false)
return;
var count = --_counter[instanceType];
//if (instanceType == typeof(IntervalTimerGate))
// DebugX.LogError("~", instanceType.Name, _counter[instanceType]);
if (count <= 0)
_counter.Remove(instanceType);
}
}
public class TestClass : GCWMonitor { }
public class Test : MonoBehaviour
{
private List<TestClass> _lst = new List<TestClass>();
[Button]
private void Function()
{
_lst.Add(new TestClass());
}
[Button]
private void Destroy()
{
Destroy(gameObject);
}
}
I cached TestClass instances to the list of Test(Monobehaviour)
and the counter ascended properly.
but when I Destroyed the gameObject of Test(Monobehaviour)
the count did not descend which means the destructor not called.
I did tried calling GC.Collect() function after destroying the gameObject.
but didn't work
the destructor was called only when I added this script.
private void OnDestroy()
{
_lst.Clear();
}
This was a CHAOTIC mind blower.
because I thought destroying the GameObject will also destroy MonoBehaviour Script
and will release all the raw class instance(member varaible) references.
===So My Question is===
why does the instance(raw class) reference is remained after the gameObject is destroyed?
could it be a bug of UNITY??
or is there a settings that I should know?
I could put a release code on "OnDestroy, or some circumstances" every time.
But it feels something odd.
Something that has been bothering me for a long time is why do the following lines of code have the same results.
Code 1:
Transform[] childs = gameObject.GetComponentsInChildren<Transform>();
foreach(Transform child in childs) { Debug.Log(child.name); }
Code 2:
foreach(Transform child in gameObject.transform) { Debug.Log(child.name); }
This is actuallly a pseudo-code, I didn't really test it but should be enough to explain.
My question is, what's happening on Code 2 ? Is gameObject.transform actually an array of Transform ? Why doesn't Code 2 print the name of the parent gameObject ?
Maybe this is something very simple and obvious I'm just overlooking but I can't make it out right now.
Transform implements the IEnumerable interface. This interface is what allows the use of the foreach keyword.
public partial class Transform : Component, IEnumerable
The IEnumerable interface requires implementation of the GetEnumerator() method. The enumerator is responsible for keeping track of the position in the underlying collection and indicating if there are more items to be iterated over.
This is implemented in Transform below
public IEnumerator GetEnumerator()
{
return new Transform.Enumerator(this);
}
private class Enumerator : IEnumerator
{
Transform outer;
int currentIndex = -1;
internal Enumerator(Transform outer)
{
this.outer = outer;
}
//*undocumented*
public object Current
{
get { return outer.GetChild(currentIndex); }
}
//*undocumented*
public bool MoveNext()
{
int childCount = outer.childCount;
return ++currentIndex < childCount;
}
//*undocumented*
public void Reset() { currentIndex = -1; }
}
I have a ScriptableObject reference in a MonoBehaviour class.
Modify its values with CustomEditor button.
Save It.
Values seems changed.
Even selected ScriptableObject Inspector displays data modified.
But asset file on disk don't have any data.
And there's no data available from code of any another MonoBehaviour instance.
Here is a code snippet used to save my ScriptableObject.
public TextAsset text;
[SerializeField]
//Thats My Scriptable Object
public PathFramesList pathFramesList;
[SerializeField]
public List<List<ICurveData>> frameDatasList = new List<List<ICurveData>>();
// Called By CustomEditor Button
public void ReadData()
{
#region BlahBlahBlah generating some data to save
var separator = new string[] { "%" };
var framesUnparsedData = text.text.Split(separator, StringSplitOptions.None);
foreach (var f in framesUnparsedData)
frameDatasList.Add(ParseFrame(f));
var result = new ICurveData[frameDatasList.Count][];
for (int i = 0; i < frameDatasList.Count; i++)
result[i] = frameDatasList[i].ToArray();
#endregion
#region Trying to save data
pathFramesList.frameDatasList = result; // Set data to object
EditorUtility.SetDirty(pathFramesList); // Even This didn't help
AssetDatabase.SaveAssets(); // Looks it doesn't work
AssetDatabase.Refresh(); // Hoped this will help, whatever it do
#endregion
}
And that's my ScriptableObjectClass
And Its CustomEditor
[CreateAssetMenu(fileName = "ParsedPathData", menuName = "Create Path Data Asset")]
[System.Serializable]
public class PathFramesList : ScriptableObject
{
[SerializeField]
public ICurveData[][] frameDatasList;
}
#if UNITY_EDITOR
[CustomEditor(typeof(PathFramesList))]
public class PathFramesListEditor : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
PathFramesList pathFrameList = (PathFramesList)target;
if(pathFrameList.frameDatasList == null)
{
GUILayout.TextField("Frames Count is 0");
}
else
{
// It still has all data
GUILayout.TextField("Frames Count is " + pathFrameList.frameDatasList.Length, 200);
}
if(GUILayout.Button("SaveAsset"))
{
// Tried to force saving with this) Didn't help
EditorUtility.SetDirty(target);
EditorUtility.SetDirty(serializedObject.targetObject);
serializedObject.Update();
serializedObject.ApplyModifiedProperties();
AssetDatabase.SaveAssets();
}
}
}
#endif
Ok.
Looks like scriptable objects files just can't store interface implementations examples.
Any else data was written to file well,
Correct me, please, if i'm wrong.
I am fairly new to Unity and C# and am having some trouble. I am designing a 2d game, which has multiple levels. Each level contains a LevelManager which stores whether the level has been completed or not. They also have the DontDestroyOnLoad command in them. I want to access all the LevelManager gameObjects in my game and then store them in a level select scene. I want to use the win/lose bool to determine if the level has been completed so I can unlock the next level. To be clear, I want a way to access all the LevelManagers in my ENTIRE game and then store them as an array in a GameManager script. How do I do that?
Below is the LevelManager script which declares whether the level has been won or not.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneController : MonoBehaviour
{
private GameObject[] StartHouseCount;
private GameObject[] StartDragonCount;
private GameObject[] LiveDragonCount;
private GameObject[] FinishedHouseCount;
public int NumOfHouses;
public int NumOfFinishedHouse;
public int NumOfDragons;
public int LiveNumOfDragons;
public GameObject[] Players;
public GameObject CurrentPlayer;
[Header("Player")]
public float RefuelRate;
public float RepairRate;
public GameObject canvas;
public bool GameIsPaused = false;
private GameObject MainPlayer;
public bool Win = false;
public int Level;
// Start is called before the first frame update
void Start()
{
CurrentPlayer = Players[0];
StartHouseCount = GameObject.FindGameObjectsWithTag("House");
StartDragonCount = GameObject.FindGameObjectsWithTag("Dragon");
NumOfHouses = StartHouseCount.Length;
NumOfDragons = StartDragonCount.Length;
MainPlayer = Players[0];
}
// Update is called once per frame
void Update()
{
GameIsPaused = canvas.GetComponent<PauseMenu>().GameIsPaused;
LiveDragonCount = GameObject.FindGameObjectsWithTag("Dragon");
LiveNumOfDragons = LiveDragonCount.Length;
FinishedHouseCount = GameObject.FindGameObjectsWithTag("ThankYou");
NumOfFinishedHouse = FinishedHouseCount.Length;
if (Input.GetKeyDown(KeyCode.Alpha1))
{
CurrentPlayer = Players[0];
}
if (Input.GetKeyDown(KeyCode.Alpha2))
{
CurrentPlayer = Players[1];
}
if (NumOfFinishedHouse == NumOfHouses)
{
SceneManager.LoadScene("WinScene");
}
if (MainPlayer == null)
{
SceneManager.LoadScene("LoseScene");
}
if (MainPlayer.GetComponent<BasicHelicopterController>().CurrentFuel <= 0 || MainPlayer.GetComponent<BasicHelicopterController>().CurrentHealth <= 0)
{
SceneManager.LoadScene("LoseScene");
}
}
}
You can create a ScoreManager script to store scores of each level using PlayerPrefs and attach this script to a GameObject in the first scene.
public class ScoreManager: MonoBehavior {
public static ScoreManager Instance;
const string LEVEL_KEY = "SCORE_";
const string MAX_LEVEL_KEY = "MAX_LEVEL";
public static int MAX_LEVEL {
get { return PlayerPrefs.GetInt(MAX_LEVEL_KEY, 1); }
set {
if(value > MAX_LEVEL) {
PlayerPrefs.SetInt(MAX_LEVEL_KEY, value);
}
}
}
void Awake() {
if(Instance != null) {
Destroy(this.gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(this);
}
public void SaveScore(int level, float score) {
PlayerPrefs.SetFloat(LEVEL_KEY + level, score);
MAX_LEVEL = level;
}
public float GetScore(int level) {
return PlayerPrefs.GetFloat(LEVEL_KEY + level, 0);
}
public bool IsLevelUnlocked(int level) {
// You can write your own level-unlock logic here.
return level <= MAX_LEVEL;
}
...
}
Then you can just call its functions and properties from other scripts.
...
public void GameOver() {
ScoreManager.Instance.SaveScore(level, score);
}
...
Having all LevelManger scripts with DontDestroyOnLoad will cause a memory leak and sometimes it will affect the game performance.
First, DontDestroyOnLoad is not supposed to do this - main usage for this is to implement things like unity-singleton (easy-to-use template). It means, you`ll have one instance of levelManager, not one per level.
To do it in your way, you need to load all the scenes additively, and get all the LevelManager instances. Or you can load all scenes one-by-one, getting level name and it`s isPassed value from current LevelManager. It can take a while, and its a wrong way.
The right way is to store inter-scene data in ScriptableObject models. It`s like a MonoBehaviour, but not related to scene - it just lays somewhere in project.
So, in your case, you can create ScriptableObject called LevelProgress. It will store the list of all passed levels. It will have public method LevelPassed(string levelName), as a parameter you can use anything - levels name, levels index in build, scene itself, whatever. Every time, when player passes any level, your LevelManager (remove DontDestoryOnLoad from it), which has a reference to LevelProgress scriptable object, will call LevelProgress.LevelPasses(currentLevel). And then, if you need to know, was some level passed, or not, you can just check, is LevelProgress.PassedLevels list contains this level.
Additionally, you need to implement between-sessions persistance for this scriptable object. IMO, the best way for this is to use JSONUtility to convert ScriptableObject into JSON string, and then write it to PlayerPrefs.
I seemed to have figured it out. I created a gameObject containing a GameManagement script, that had the DontDestroyOnLoad line. I had also added a specific tag. I then searched for that object in each level and updated my values to that. The GameManagement script had an array of bools for levels completed and levels unlocked. Each levelmanager decided whether the level was won and updated that. Using that I determined what level was unlocked. I did though need to use Fire King's Awake Command. It ensures that there are no other copies of the script in the game. Solves my problems.
What happens when you add an Action to a Dictionary from another object?
First off, I'm trying to design some decent in-game context menus. My goal is to dynamically generate each item. Each item is loaded from a Dictionary that stores Actions. The dictionary is accessed from up to 3 components of each gameObject with a GamePiece component attached.
First, there is a dictionary with Actions living as a component of each type GamePiece:
public class GamePiece : MonoBehaviour {
protected bool rightClickable = true;
protected StatManager statManager;
Transform ui;
CanvasManager canvas;
SpriteRenderer sprite;
Color spriteColor;
public Dictionary<string, Action> actions;
void Awake(){
statManager = GameObject.Find("StatPanel").GetComponent<StatManager>();
actions = new Dictionary<string, Action>();
actions.Add("Deconstruct", Deconstruct);
}
The problem is that no matter how I populate the dictionary, the last added dictionary item is called. So if I were to add a "Destroy()" call and a "SupplyPower()" call. The dictionary will only ever call "Supply Power". This is particularly odd because the menus themselves are displaying the correct buttons.
I suspect the problem to be that I'm adding dictionary items from other components on the same gameObject. For instance, The GamePiece component holds the dictionary and adds some basic actions, and then a Generator component will access that an add a reference to it's own SupplyPower() method
public class Generator: MonoBehaviour {
public Structure structure;
void Start () {
structure = gameObject.GetComponent<Structure>();
structure.gamePiece.actions.Add("Supply Power", SupplyPower);
}
}
So here's what happens when the context menu is created:
public class ContextMenu : MonoBehaviour {
public Transform menuItem;
//Takes a ref from the calling gameObject, t. And is called from CanvasManager.cs
public void PopulateContextMenu(GameObject t)
{
Transform selections = transform.FindChild("Selections").transform; //Parent for items.
//gamePiece holds the dictionary.
GamePiece gamePiece = t.GetComponent<GamePiece>();
foreach (KeyValuePair<string, Action> kVp in gamePiece.actions)
{
GameObject menuItem =
(GameObject)Instantiate(Resources.Load("MenuItem"));
menuItem.name = kVp.Key;
menuItem.GetComponent<Text>().text = kVp.Key;
//Adding functuionality.
menuItem.GetComponent<Button>().onClick.AddListener
(() => { kVp.Value.Invoke(); });
menuItem.GetComponent<Button>().onClick.AddListener
(() => { CloseContextMenu(); });
menuItem.transform.SetParent(selections, false);
}
}
public void CloseContextMenu()
{
Destroy(this.gameObject);
}
}
The PopulateContextMenu function is called from the CanvasManager class:
public class CanvasManager : MonoBehaviour {
public void ToggleContextMenu(GameObject t) {
GameObject newMenu = (GameObject)Resources.Load("ContextMenu");
newMenu = Instantiate(newMenu) as GameObject;
//Passing gameObject t into PopulatContextMenu
newMenu.GetComponent<ContextMenu>().PopulateContextMenu(t);
}
}
Here, ToggleContextMenu() is called from the gameObjects OnMouseOver() callback:
public class GamePiece : MonoBehaviour {
void OnMouseOver(){
if (Input.GetMouseButtonDown(1) && rightClickable) {
canvas.ToggleContextMenu(this.gameObject);
}
}
}
So when this is called, it's passing a reference to itself to the CanvasManager and then getting handed off the ContextMenu.
Store the Actions locally before invoking and you're good to go.
In ContextMenu:
Action newAction =kVp.Value;
menuItem.GetComponent<Button>().onClick.AddListener(() => {newAction.Invoke();});