- Home /
Editor Script, Index Out of Range Exception after Play
Can somebody explain me why a CustomEditor based script is working fine in Editor Mode, but when I switch to PlayMode it throws me back a Index Out of Range Exception?
IndexOutOfRangeException: Array index is out of range.
TerrainPrototypesGroupManagerEDITOR.OnInspectorGUI () (at Assets/Editor/TerrainPrototypesGroupManagerEDITOR.cs:50)
UnityEditor.InspectorWindow.DrawEditor (UnityEditor.Editor[] editors, Int32 editorIndex, Boolean rebuildOptimizedGUIBlock, System.Boolean& showImportedObjectBarNext, UnityEngine.Rect& importedObjectBarRect) (at /Users/builduser/buildslave/unity/build/Editor/Mono/Inspector/InspectorWindow.cs:1374)
UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr)
//my struct, where the error appears.
[System.Serializable]public struct list_TreeInstances {[SerializeField]public List < TreeInstance > m_TreeInstances;}
I've got a second struct which is kind of identical, but uses a GameObject instead of TreeInstance, where I don't have the null reference. Could this be because its properly used with a SerializedProperty in Inspector? like:
//_GO_Groups is an Array ( list_GameObject[] _GO_Groups = new list_GameObject[0];)
SerializedProperty s_GO_Groups = serializedObject.FindProperty("_GO_Groups");
// example
SerializedProperty s0 = s_GO_Groups.GetArrayElementAtIndex(master).FindPropertyRelative("m_GameObjects");
for(int i = 0; i<s0.arraySize;i++){
SerializedProperty s1 = s0.GetArrayElementAtIndex(i);
if(s1.objectReferenceValue != null){
EditorGUILayout.PropertyField(s1,new GUIContent(_TPGM._GO_Groups[master].m_GameObjects[i].name));
}
Well final and important Question, whats the best way to find the Source of causing this trouble? Sorry to tag you but, I know you both are Genius in this @JVene @Bunny83
full code is here if needed
@hexagonius please don’t close that many questions.. i dont $$anonymous$$d if you close bad wrtitten and off topic questions.. but the rest i’m getting quite anoyed. like a few days you closed this question. with the link to just the docs.. well i know what a null reference exception is.. but in this case you just didn’t think about the question alot. in the end it was due to a serialized problem inside of unity(have a look below)
you're right, that was a mistake here since the cause layed further down the path and was more difficult to solve than a regular case.
but co$$anonymous$$g by the amount of questions that are asked there's not much time you have if you want to help a lot of people. the throughout quality of them is bad and repetitive. I apologize for this one.
i defently know what you mean..
alot of questions are like that
Answer by Bunny83 · Oct 22, 2018 at 12:54 AM
Unity's TreeInstance struct is not serializable. The TerrainData class is implemented in native code and is serialized in native code. The managed class is just a wrapper of the serialized native data.
When you read the treeInstances property you actually call a native method which returns the instances from native code.
So since the struct isn't marked with the Serializable attribute it's not a managed type that the Unity serializer can serialize. Therefore the List<TreeInstance>
is not a serializable type either. That means whenever your data needs to be serialized / deserialized this will not be included. When you enter playmode everything gets serialized and reloaded from serialized data. That means your List will be null since it can't be serialized. You may need to create a wrapper type that can be serialized which holds the same information as the TreeInstance struct and copy the data as needed.
edit
As i said in the comment below, i would implement the conversion inside your wrapper struct. Since this is only for serialization i would use a constructor for the creation of the wrapper and an implicit conversion operator to turn our wrapper back to a TreeInstance value.
[System.Serializable]
public struct TreeInstanceArray
{
public Vector3 position;
public float widthScale;
public float heightScale;
public float rotation;
public Color32 color;
public Color32 lightmapColor;
public int prototypeIndex;
public TreeInstanceArray(TreeInstance aTree)
{
position = aTree.position;
widthScale = aTree.widthScale;
heightScale = aTree.heightScale;
rotation = aTree.rotation;
color = aTree.color;
lightmapColor = aTree.lightmapColor;
prototypeIndex = aTree.prototypeIndex;
}
public static implicit operator TreeInstance (TreeInstanceArray aTree)
{
TreeInstance inst = new TreeInstance;
inst.position = aTree.position;
inst.widthScale = aTree.widthScale;
inst.heightScale = aTree.heightScale;
inst.rotation = aTree.rotation;
inst.color = aTree.color;
inst.lightmapColor = aTree.lightmapColor;
inst.prototypeIndex = aTree.prototypeIndex;
return inst;
}
}
Now if you have a "TreeInstanceArray" value you can simply assign or use it where a TreeInstance value would be required.
TreeInstanceArray serializedValue = new TreeInstanceArray();
// this will trigger the implicit conversion
TreeInstance tree = serializedValue;
List<TreeInstance> trees;
// this will also trigger the implicit conversion
trees.Add(serializedValue);
To turn a TreeInstance into a "TreeInstanceArray", just use the constructor:
TreeInstanceArray serializedValue = new TreeInstanceArray(someTreeInstance);
Well might you could add a comment to the way I did it.
I build a new struct in my static class. [System.Serializable]public struct TreeInstanceArray {
[SerializeField] public Vector3 position;
[SerializeField] public float widthScale;
[SerializeField] public float heightScale;
[SerializeField] public float rotation;
[SerializeField] public Color32 color;
[SerializeField] public Color32 lightmapColor;
[SerializeField] public int prototypeIndex;
}
and made a get/set function which exchange data between TerrainData.TreeInstances and the new TreeInstanceArray..
void GetTI(){
_TreeInstances = new CSTPG$$anonymous$$.TreeInstanceArray[_ted.treeInstances.Length];
for(int i = 0; i<_ted.treeInstances.Length; i++){
TreeInstance t = _ted.treeInstances[i];
_TreeInstances[i].position = t.position;
_TreeInstances[i].widthScale = t.widthScale;
_TreeInstances[i].heightScale = t.heightScale;
_TreeInstances[i].rotation = t.rotation;
_TreeInstances[i].color = t.color;
_TreeInstances[i].lightmapColor = t.lightmapColor;
_TreeInstances[i].prototypeIndex = t.prototypeIndex;
}
}
void SetTI(){
_ted.treeInstances = new TreeInstance[_TreeInstances.Length];
for(int i = 0; i<_TreeInstances.Length; i++){
TreeInstance t = _ted.treeInstances[i];
t.position = _TreeInstances[i].position;
t.widthScale = _TreeInstances[i].widthScale;
t.heightScale = _TreeInstances[i].heightScale;
t.rotation = _TreeInstances[i].rotation;
t.color = _TreeInstances[i].color;
t.lightmapColor = _TreeInstances[i].lightmapColor;
t.prototypeIndex = _TreeInstances[i].prototypeIndex;
}
}
right now it works quite good, but I think I could optimize the get / set function
but the important thing is you gave the right input for a solution. so I'm really really thankful!!!
dan
I think it would make more sense to build the conversion into your wrapper class / struct. C# has type conversion operators which come in quite handy in such cases. Unity for example implemented them for Color / Color32 conversion or Vector2 / 3 / 4 conversion. I'll add an example to my answer.
Note with the implementation i've shown in my answer your Get and Set method could be just:
void GetTI()
{
_TreeInstances = new CSTPG$$anonymous$$.TreeInstanceArray[_ted.treeInstances.Length];
for(int i = 0; i < _ted.treeInstances.Length; i++)
{
_TreeInstances[i] = new CSTPG$$anonymous$$.TreeInstanceArray(_ted.treeInstances[i]);
}
}
void SetTI()
{
TreeInstance[] trees = new TreeInstance[_TreeInstances.Length];
for(int i = 0; i < _TreeInstances.Length; i++)
{
// implicit conversion
trees[i] = _TreeInstances[i];
}
_ted.treeInstances = trees;
}
Note i just realised in your comment there's an error. Your SetTI method won't work TreeInstance is a struct. When you read " _ted.treeInstances[i]" into the local variable "t" you get a copy of that variable. You then change the members of "t" but you never actually change the value in "_ted.treeInstances[i]"
In addition the "treeInstances" property of the TerrainData object is a property. The array that is returned is a copy of the internal data. So changing this array won't affect the actual data. You have to assign an array of TreeInstances to the property in order to invoke the property setter method.
Thanks for the detailed Edit and Solution to build a Wrapper!
right now a single TreeInstance conversion is working as expected, but if I want to convert a Array back to the the TerrainData.treeInstances Array it throws an conversion error.
//treeInstances and serializedValue are Arrays
//What I want to do:
TerrainData.treeInstances = serializedValue;
// right now I have to do this
TerrainData.treeInstances = new TreeInstance[serializedValue.Length];
for(int i = 0; i<serializedValue.Length; i++){
TerrainData.treeInstances[i] = serializedValue[i];
}
Have you seen my last comment? This line doesn't work as i mentioned at the bottom of my last comment:
TerrainData.treeInstances[i] = serializedValue[i];
That's because "TerrainData.treeInstances" is a property. Whenever you "read" that property it will call a native method which returns a copy of the internal array. So changes to that array has no effect. The property also has a set method which will allows to actually pass a temporary array with the changed instances back to the native code. That's what i actually do in the SetTI()
method i've posted in my comment. Notice the temporary array and the assignment at the end:
_ted.treeInstances = trees; // this will invoke the setter of the property.
With Linq the array conversion could be done in a single line, but it's actually a little bit more expensive and creates more temporary garbage:
TerrainData.treeInstances = serializedValue.Select(t=>(TreeInstance)t).ToArray();
For this to work you need using System.Linq;
at the top. The Select method is an extension for collections which takes a delegate which will be invoked for every element of the collection. "Select" specifically is able to return something completely different. So the return value of Select will be a new IEnumerable that contains this "new type" that was returned by the delegate. Finally we can convert the IEnumerable back into an array by ToArray. Of course all those linq enumerables requires some memory, however the thing which requires the most additional memory is the ToArray method. it actually does the same as a generic List. It iterates through the IEnumerable and adds the elements one by one and expands its internal buffer as needed. Finally a new array with the correct number of elemens is created and the values are copied over. If this doesn't happen once every frame you can go with the linq solution. However if it's time critical you better do the loop manually. Further more if you do this conversion / assignment a lot of times you may even cache the temporary array and reuse it unless the required size changes.
Though i'm a bit confused where you actually use this code. In your original question you were using the SerializedObject / SerializedProperty in the inspector. Direct object access is possible but can cause issues when you combine it with the SerializedObject / Property.
Your answer
Follow this Question
Related Questions
Custom Editor Script resets values on Play 1 Answer
How to access one class instance in editor script? 1 Answer
How To Force an Inspector to Repaint 3 Answers
TexturePropertySingleLine in Editor class 0 Answers
Gizmos.DrawLine is dissapearing after returning to editor after Playing the scene 0 Answers