I am learning something about Serialization in Unity, and know that ISerializationCallbackReceiver can be used to help serialize some complex data structure that cannot serialize directly. I have tested the following code in Unity 2017 and Find some problems.
public class SerializationCallbackScript : MonoBehaviour, ISerializationCallbackReceiver
{
public List<int> _keys = new List<int> { 3, 4, 5 };
public List<string> _values = new List<string> { "I", "Love", "Unity" };
//Unity doesn't know how to serialize a Dictionary
public Dictionary<int, string> _myDictionary = new Dictionary<int, string>();
public void OnBeforeSerialize()
{
_keys.Clear();
_values.Clear();
foreach (var kvp in _myDictionary)
{
_keys.Add(kvp.Key);
_values.Add(kvp.Value);
}
}
public void OnAfterDeserialize()
{
_myDictionary = new Dictionary<int, string>();
for (int i = 0; i != Math.Min(_keys.Count, _values.Count); i++)
_myDictionary.Add(_keys[i], _values[i]);
}
void OnGUI()
{
foreach (var kvp in _myDictionary)
GUILayout.Label("Key: " + kvp.Key + " value: " + kvp.Value);
}
}
Obviously, this example show me how to serialize a dictionary by changing it into a list and resuming when deserialize. However, when i run the codes pratically, I find _keys and _values both empty (keys.Count = 0) in inspector. Even worse, i cannot modify their values in the inspector. As a result, nothing appears when entering the play mode.
So I want to know what is the actual usage of ISerializationCallbackReceiver and the reason of what happened.
OnBeforeSerialize is called in the editor every time the inspector updates (basically always), and it calls clear on your keys and value lists
i changed this for me like that:
#if UNITY_EDITOR
using UnityEditor;
#endif
public class ....{
public void OnBeforeSerialize() {
#if UNITY_EDITOR
if(!EditorApplication.isPlaying
&& !EditorApplication.isUpdating
&& !EditorApplication.isCompiling) return;
#endif
...
so it does not clear on edit-time.
Related
It appears that anything I add to a List<string[]> will get added, but when I save any scripts and Unity does compiles everything, the items in the list disappears.
Here is a simple class I wrote that just displays a window and adds labels according to how many items are in the list:
public class TestEditorWindow : EditorWindow
{
string windowLabel = "Test Window";
[SerializeField] List<string[]> myList = new List<string[]>();
[MenuItem("Tools/My Window")]
static void Init()
{
TestEditorWindow myWindow = (TestEditorWindow)GetWindow(typeof(TestEditorWindow));
myWindow.Show();
}
private void OnGUI()
{
GUILayout.Label(windowLabel, EditorStyles.boldLabel);
EditorGUILayout.Separator();
GUILayout.BeginVertical("box", GUILayout.ExpandWidth(true));
for(int i = 0; i < myList.Count; i++)
{
EditorGUILayout.LabelField("Stupid");
}
if(GUILayout.Button("+", GUILayout.MaxWidth(30)))
{
//myList.Add(new string[2]); //<-- Also tried it this way
myList.Add(new string[] { "" });
}
GUILayout.EndVertical();
}
}
The window shows and every time I hit the button a new label is added to the window, but as soon as Unity compiles anything, the values go away.
If I change the list to List<string> it behaves as intended
I've also tried setting up the list like so and got the same results:
[SerializeField] static List<string[]> myList;
[MenuItem("Tools/My Window")]
static void Init()
{
myList = new List<string[]>();
TestEditorWindow myWindow = (TestEditorWindow)GetWindow(typeof(TestEditorWindow));
myWindow.Show();
}
Am I doing something wrong with how I'm loading the list?
Unity cannot serialize multidimensional collections.
There is a work around though.
Create a new class that contains the string array, and create a list using that type.
[System.Serializable]
public class StringArray
{
public string[] array;
}
and in your window use:
public List<StringArray> myList = new List<StringArray>();
I tried to make a dynamic scroll view to display selectable options that can be both generated or destroyed during runtime but for some reason it only lets me to generate up to three, otherwise I get a Missing Reference Exception.
When I generate one then delete it, it will also give a Missing Reference Exception.
But when I generate two, then delete one or even two, everything works just fine no matter what I do.
How can I fix this strange Behaviour?
The code:
private List<JL> JLList;
public List<GameObject> JLID;
public GameObject ButtonPrefab;
public GameObject PrefabParent;
public GameObject JLMenuManager;
public GameObject ScrollViewContent;
private void Update()
{
JLList = JLMenuManager.GetComponent<AddJL>().JLList;
CheckForChange();
}
private void GenerateOptions()
{
foreach (Transform child in ScrollViewContent.transform)
{
Destroy(child.gameObject);
}
for (int i = 0; i < JLList.Count; i++)
{
GameObject newJLOption = Instantiate(ButtonPrefab, PrefabParent.transform);
JLID.Add(newJLOption);
newJLOption = JLID[i];
int JLIndex = i;
newJLOption.GetComponent<Button>().onClick.AddListener(() => LoadJLOptions(JLIndex));
}
}
private void LoadJLOptions(int JLIndex)
{
SendMessage("SetJL", JLIndex);
Debug.Log(JLIndex);
}
private void CheckForChange()
{
int allJLOptions = ScrollViewContent.transform.childCount;
if (allJLOptions != JLList.Count)
{
GenerateOptions();
}
}
It seems like it is only spawning in how many JLList.Count is set to, open your script JLList and there should be a variable called count . Check how many that is set to. If it is 3, then that is your issue. If not, then follow what #BugFinder said and try remove them from the list
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'm working on a small program that can modify the animation at run time(Such as when you run faster the animation not only play faster but also with larger movement). So i need to get the existing animation, change its value, then send it back.
I found it is interesting that i can set a new curve to the animation, but i can't get access to what i already have. So I either write a file to store my animation curve (as text file for example), or i find someway to read the animation on start up.
I tried to use
AnimationUtility.GetCurveBindings(AnimationCurve);
It worked in my testing, but in some page it says this is a "Editor code", that if i build the project into a standalone program it will not work anymore. Is that true? If so, is there any way to get the curve at run time?
Thanks to the clearify from Benjamin Zach and suggestion from TehMightyPotato
I'd like to keep the idea about modifying the animation at runtime. Because it could adapt to more situations imo.
My idea for now is to write a piece of editor code that can read from the curve in Editor and output all necesseary information about the curve (keyframes) into a text file. Then read that file at runtime and create new curve to overwrite the existing one. I will leave this question open for a few days and check it to see if anyone has a better idea about it.
As said already AnimationUtility belongs to the UnityEditor namespace. This entire namespace is completely stripped of in a build and nothing in it will be available in the final app but only within the Unity Editor.
Store AnimationCurves to file
In order to store all needed information to a file you could have a script for once serializing your specific animation curve(s) in the editor before building using e.g. BinaryFormatter.Serialize. Then later on runtime you can use BinaryFormatter.Deserialize for returning the info list again.
If you wanted it more editable you could as well use e.g. JSON or XML of course
UPDATE: In general Stop using BinaryFormatter!
In the newest Unity versions the Newtonsoft Json.NET package comes already preinstalled so simply rather use JSON
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Unity.Plastic.Newtonsoft.Json;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
public class AnimationCurveManager : MonoBehaviour
{
[Serializable]
public sealed class ClipInfo
{
public int ClipInstanceID;
public List<CurveInfo> CurveInfos = new List<CurveInfo>();
// default constructor is sometimes required for (de)serialization
public ClipInfo() { }
public ClipInfo(Object clip, List<CurveInfo> curveInfos)
{
ClipInstanceID = clip.GetInstanceID();
CurveInfos = curveInfos;
}
}
[Serializable]
public sealed class CurveInfo
{
public string PathKey;
public List<KeyFrameInfo> Keys = new List<KeyFrameInfo>();
public WrapMode PreWrapMode;
public WrapMode PostWrapMode;
// default constructor is sometimes required for (de)serialization
public CurveInfo() { }
public CurveInfo(string pathKey, AnimationCurve curve)
{
PathKey = pathKey;
foreach (var keyframe in curve.keys)
{
Keys.Add(new KeyFrameInfo(keyframe));
}
PreWrapMode = curve.preWrapMode;
PostWrapMode = curve.postWrapMode;
}
}
[Serializable]
public sealed class KeyFrameInfo
{
public float Value;
public float InTangent;
public float InWeight;
public float OutTangent;
public float OutWeight;
public float Time;
public WeightedMode WeightedMode;
// default constructor is sometimes required for (de)serialization
public KeyFrameInfo() { }
public KeyFrameInfo(Keyframe keyframe)
{
Value = keyframe.value;
InTangent = keyframe.inTangent;
InWeight = keyframe.inWeight;
OutTangent = keyframe.outTangent;
OutWeight = keyframe.outWeight;
Time = keyframe.time;
WeightedMode = keyframe.weightedMode;
}
}
// I know ... singleton .. but what choices do we have? ;)
private static AnimationCurveManager _instance;
public static AnimationCurveManager Instance
{
get
{
// lazy initialization/instantiation
if (_instance) return _instance;
_instance = FindObjectOfType<AnimationCurveManager>();
if (_instance) return _instance;
_instance = new GameObject("AnimationCurveManager").AddComponent<AnimationCurveManager>();
return _instance;
}
}
// Clips to manage e.g. reference these via the Inspector
public List<AnimationClip> clips = new List<AnimationClip>();
// every animation curve belongs to a specific clip and
// a specific property of a specific component on a specific object
// for making this easier lets simply use a combined string as key
private string CurveKey(string pathToObject, Type type, string propertyName)
{
return $"{pathToObject}:{type.FullName}:{propertyName}";
}
public List<ClipInfo> ClipCurves = new List<ClipInfo>();
private string filePath = Path.Combine(Application.streamingAssetsPath, "AnimationCurves.dat");
private void Awake()
{
if (_instance && _instance != this)
{
Debug.LogWarning("Multiple Instances of AnimationCurveManager! Will ignore this one!", this);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
// load infos on runtime
LoadClipCurves();
}
#if UNITY_EDITOR
// Call this from the ContextMenu (or later via editor script)
[ContextMenu("Save Animation Curves")]
private void SaveAnimationCurves()
{
ClipCurves.Clear();
foreach (var clip in clips)
{
var curveInfos = new List<CurveInfo>();
ClipCurves.Add(new ClipInfo(clip, curveInfos));
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
{
var key = CurveKey(binding.path, binding.type, binding.propertyName);
var curve = AnimationUtility.GetEditorCurve(clip, binding);
curveInfos.Add(new CurveInfo(key, curve));
}
}
// create the StreamingAssets folder if it does not exist
try
{
if (!Directory.Exists(Application.streamingAssetsPath))
{
Directory.CreateDirectory(Application.streamingAssetsPath);
}
}
catch (IOException ex)
{
Debug.LogError(ex.Message);
}
// create a new file e.g. AnimationCurves.dat in the StreamingAssets folder
var json = JsonConvert.SerializeObject(ClipCurves);
File.WriteAllText(filePath, json);
AssetDatabase.Refresh();
}
#endif
private void LoadClipCurves()
{
if (!File.Exists(filePath))
{
Debug.LogErrorFormat(this, "File \"{0}\" not found!", filePath);
return;
}
var fileStream = new FileStream(filePath, FileMode.Open);
var json = File.ReadAllText(filePath);
ClipCurves = JsonConvert.DeserializeObject<List<ClipInfo>>(json);
}
// now for getting a specific clip's curves
public AnimationCurve GetCurve(AnimationClip clip, string pathToObject, Type type, string propertyName)
{
// either not loaded yet or error -> try again
if (ClipCurves == null || ClipCurves.Count == 0) LoadClipCurves();
// still null? -> error
if (ClipCurves == null || ClipCurves.Count == 0)
{
Debug.LogError("Apparantly no clipCurves loaded!");
return null;
}
var clipInfo = ClipCurves.FirstOrDefault(ci => ci.ClipInstanceID == clip.GetInstanceID());
// does this clip exist in the dictionary?
if (clipInfo == null)
{
Debug.LogErrorFormat(this, "The clip \"{0}\" was not found in clipCurves!", clip.name);
return null;
}
var key = CurveKey(pathToObject, type, propertyName);
var curveInfo = clipInfo.CurveInfos.FirstOrDefault(c => string.Equals(c.PathKey, key));
// does the curve key exist for the clip?
if (curveInfo == null)
{
Debug.LogErrorFormat(this, "The key \"{0}\" was not found for clip \"{1}\"", key, clip.name);
return null;
}
var keyframes = new Keyframe[curveInfo.Keys.Count];
for (var i = 0; i < curveInfo.Keys.Count; i++)
{
var keyframe = curveInfo.Keys[i];
keyframes[i] = new Keyframe(keyframe.Time, keyframe.Value, keyframe.InTangent, keyframe.OutTangent, keyframe.InWeight, keyframe.OutWeight)
{
weightedMode = keyframe.WeightedMode
};
}
var curve = new AnimationCurve(keyframes)
{
postWrapMode = curveInfo.PostWrapMode,
preWrapMode = curveInfo.PreWrapMode
};
// otherwise finally return the AnimationCurve
return curve;
}
}
Then you can do something like e.e.
AnimationCurve originalCurve = AnimationCurvesManager.Instance.GetCurve(
clip,
"some/relative/GameObject",
typeof<SomeComponnet>,
"somePropertyName"
);
the second parameter pathToObject is an empty string if the property/component is attached to the root object itself. Otherwise it is given in the hierachy path as usual for Unity like e.g. "ChildName/FurtherChildName".
Now you can change the values and assign a new curve on runtime.
Assigning new curve on runtime
On runtime you can use animator.runtimeanimatorController in order to retrieve a RuntimeAnimatorController reference.
It has a property animationClips which returns all AnimationClips assigned to this controller.
You could then use e.g. Linq FirstOrDefault in order to find a specific AnimationClip by name and finally use AnimationClip.SetCurve to assign a new animation curve to a certain component and property.
E.g. something like
// you need those of course
string clipName;
AnimationCurve originalCurve = AnimationCurvesManager.Instance.GetCurve(
clip,
"some/relative/GameObject",
typeof<SomeComponnet>,
"somePropertyName"
);
// TODO
AnimationCurve newCurve = SomeMagic(originalCurve);
// get the animator reference
var animator = animatorObject.GetComponent<Animator>();
// get the runtime Animation controller
var controller = animator.runtimeAnimatorController;
// get all clips
var clips = controller.animationClips;
// find the specific clip by name
// alternatively you could also get this as before using a field and
// reference the according script via the Inspector
var someClip = clips.FirstOrDefault(clip => string.Equals(clipName, clip.name));
// was found?
if(!someClip)
{
Debug.LogWarningFormat(this, "There is no clip called {0}!", clipName);
return;
}
// assign a new curve
someClip.SetCurve("relative/path/to/some/GameObject", typeof(SomeComponnet), "somePropertyName", newCurve);
Note: Typed on smartphone so no warranty! But I hope the idea gets clear...
Also checkout the example in AnimationClip.SetCurve → You might want to use the Animation component instead of an Animator in your specific use case.
I have recently been creating a game with tutorials. Unfortunately, they didn't cover a save score feature. Thanks to another user, I was able to figure out that I needed to use playerprefs. I watched tutorials online, but none of them were helpful. If you can, please help me!
Gold Per Sec Script:
using UnityEngine;
using System.Collections;
public class GoldPerSec : MonoBehaviour {
public UnityEngine.UI.Text gpsDisplay;
public Click click;
public ItemManager[] items;
void Start () {
StartCoroutine(AutoTick ());
}
void Update () {
gpsDisplay.text = GetGoldPerSec() + " Money Per Sec";
}
public float GetGoldPerSec() {
float tick = 0;
foreach (ItemManager item in items) {
tick += item.count * item.tickValue;
}
return tick;
}
public void AutoGoldPerSec() {
click.gold += GetGoldPerSec() / 10;
}
IEnumerator AutoTick() {
while (true) {
AutoGoldPerSec();
yield return new WaitForSeconds(0.10f);
}
}
}
Gold Per Click script:
using UnityEngine;
using System.Collections;
public class Click : MonoBehaviour {
public UnityEngine.UI.Text gpc;
public UnityEngine.UI.Text goldDisplay;
public float gold = 0.00f;
public int goldperclick = 1;
void Update () {
goldDisplay.text = "" + gold.ToString("F0");
gpc.text = "Money Per Click: " + goldperclick;
}
public void Clicked(){
gold += goldperclick;
}
}
My idea was for the game to save when the game is quit, and load as soon as you load the game back up. I am a complete beginner, if anyone can tell me how to do this, please tell me! Thanks! :D
You can use unity's existing functions to achieve this.
For saving data use unity's OnApplicationQuit function like this
void OnApplicationQuit() {
PlayerPrefs.SetFloat("key", value);
}
And for Restoring the values use unity's Awake function like this
void Awake(){
value = PlayerPrefs.GetFloat("key");
}
Please note that PlayerPrefs is an easy way to save data but also an very unsafe way. The player can easily manipulate his "goldValue" since it's just stored as an integer in some file on his device. PlayerPrefs should usually just be used for values the player can changed any way within in game, like volume setting etc.
EXAMPLE CODE
void Save()
{
string filename = "/filename.dat";
BinaryFormatter bf = new BinaryFormatter();
FileStream file = File.Create(Application.persistentDataPath+filename);
bf.Serialize(file, goldValue); //Use can easily use e.g. a List if you want to store more date
file.Close();
}
bool Load()
{
string filename = "/filename.dat";
if (File.Exists(Application.persistentDataPath + filename))
{
BinaryFormatter bf = new BinaryFormatter();
FileStream file = File.Open(Application.persistentDataPath + filename, FileMode.Open);
goldValue=(int) bf.Deserialize(file);
file.Close();
return true;
}
return false;
}
Add the following code to Click class:
void Awake()
{
LoadData();
}
void OnApplicationQuit()
{
SaveData();
}
void SaveData()
{
PlayerPrefs.SetFloat("gold",gold);
}
void LoadData()
{
gold = PlayerPrefs.GetFloat("gold",0f);
}