JSON loading causes WebGL build index out of range exceptions
Hi, my game has question and response text which appears at various points, which is all contained in one JSON file.
The game works in the Unity editor with no problems, and all the text appears in the right place and time.
But in the WebGL build, the first question text does appear, ie it must have loaded this correctly from the JSON file at least, but after correct answer is given, no response text appears at all, and the game is then stuck. This is a 256MB memory size development build, with full stack trace, and it produces error exact same place in Chrome or Edge browser in local host.
I'm guessing there's something wrong with JSON loading code. EDIT After @Bunny83 's very helpful comments, below is revised code for LoadJSONData.cs, the main file which loads the JSON data. However, error still remains, now with socket error also as my comment below:
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
public class LoadJSONData : MonoBehaviour
{
private string filePath;
private string jsonData = "";
[HideInInspector]
public JSONquestions jsonQuestions;
IEnumerator LoadJSON()
{
if (filePath.Contains("://") || filePath.Contains(":///"))
{
UnityWebRequest www = UnityWebRequest.Get(filePath);
yield return www.SendWebRequest();
if (www.isHttpError || www.isNetworkError)
{
Debug.LogError("There was an HTTP or network error trying to request the URI of the JSON file.");
yield return null;
}
jsonData = www.downloadHandler.text;
if (jsonData != "" && jsonData != null)
{
jsonQuestions = JsonUtility.FromJson<GameData>(jsonData).jsonQuestions;
Debug.Log("1: Just ran jsonQuestions line in LoadJSON, jsonQuestions is " + jsonQuestions + "\n And file path is " + filePath + "\n");
}
else { Debug.LogError("2: JSON file is empty for some reason! jsonData is: " + jsonData + " exactly that my friend.");
}
}
else if (System.IO.File.Exists(filePath)) {
jsonData = System.IO.File.ReadAllText(filePath);
if (jsonData != "" && jsonData != null)
{
jsonQuestions = JsonUtility.FromJson<GameData>(jsonData).jsonQuestions;
}
else { Debug.LogError("3: In LoadJSONData.cs, JSON file is empty for some reason!"); }
yield return null;
} else { Debug.LogError("4: In LoadJSONData.cs, Filepath doesnt exist it seems, so JSON file not loaded.");
yield return null;
}
}
private void Awake()
{
filePath = System.IO.Path.Combine(Application.streamingAssetsPath, "DungeonQuestions.JSON");
StartCoroutine(LoadJSON()); }
}
[System.Serializable]
public class GameData
{
public JSONquestions jsonQuestions;
}
[System.Serializable]
public class JSONquestions
{
public Qref Mary;
public Qref Farmgirl;
public Qref BatType;
public Qref JoansName;
public Qref TowerPlace;
public Qref CallLift;
public Qref EscalierButton;
public Qref Sortie;
}
[System.Serializable]
public class Qref
{
public QuestionText Qtext;
public QuestionText Ctext;
public QuestionText W1text;
public QuestionText W2text;
}
[System.Serializable]
public class QuestionText
{
public string[] phrase;
public float[] delay;
public QuestionText(string[] Phrase, float[] Delay)
{
this.phrase = Phrase; this.delay = Delay;
}
public QuestionText DeepCopy()
{
// check for array size separately
int p = this.phrase.Length;
int d = this.delay.Length;
//iterate over p and d
string[] phraseCopy = new string[p];
float[] delayCopy = new float[d];
for (int i = 0; i < p; i++)
{
phraseCopy[i] = this.phrase[i];
}
for (int j = 0; j < d; j++)
{
delayCopy[j] = this.delay[j];
}
QuestionText copy = new QuestionText(phraseCopy, delayCopy);
return copy;
}
}
The DeepCopy() methods above are designed to substitute player's input words within JSON file phrases, without altering the original text in JSON file.
EDIT2 Here are new error traces before game freezes, method called MarySuccess() which does contain a call to DeepCopy() within this method, but now not clear if DeepCopy() relevant:
blob:http://127.0.0.…9-67cd60563024:8758 IndexOutOfRangeException: Array index is out of range.
at Questions+<FillThoughtBox>c__Iterator0.MoveNext () [0x00000] in <filename unknown>:0
at UnityEngine.SetupCoroutine.InvokeMoveNext (IEnumerator enumerator, IntPtr returnValueAddress) [0x00000] in <filename unknown>:0
at UnityEngine.MonoBehaviour.StartCoroutineManaged2 (IEnumerator enumerator) [0x00000] in <filename unknown>:0
at UnityEngine.MonoBehaviour.StartCoroutine (IEnumerator routine) [0x00000] in <filename unknown>:0
at Questions.MarySet () [0x00000] in <filename unknown>:0
at Questions+SetDelegate.Invoke () [0x00000] in <filename unknown>:0
at Questions.SetQuestion (Thoughts panel, .SetDelegate setDelegate) [0x00000] in <filename unknown>:0
at PriestController3D+<Floorbound>c__Iterator0.MoveNext () [0x00000] in <filename unknown>:0
at UnityEngine.SetupCoroutine.InvokeMoveNext (IEnumerator enumerator, IntPtr returnValueAddress) [0x00000] in <filename unknown>:0
UnityEngine.MonoBehaviour:StartCoroutineManaged2(IEnumerator)
UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
Questions:MarySet()
SetDelegate:Invoke()
Questions:SetQuestion(Thoughts, SetDelegate)
<Floorbound>c__Iterator0:MoveNext()
UnityEngine.SetupCoroutine:InvokeMoveNext(IEnumerator, IntPtr)
(Filename: currently not available on il2cpp Line: -1)
blob:http://127.0.0.…-67cd60563024:13147 WebSocket connection to 'ws://192.168.1.67:54998/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED
blob:http://127.0.0.…9-67cd60563024:8758 IndexOutOfRangeException: Array index is out of range.
at Questions.MarySuccess (System.String inputText) [0x00000] in <filename unknown>:0
at Questions+ResponseDelegate.Invoke (System.String input) [0x00000] in <filename unknown>:0
at Questions.ProcessAnswer (UnityEngine.UI.InputField input, System.Collections.Generic.List`1 answers, Single clockStart, .ResponseDelegate gotItRight, .ResponseDelegate gotItWrong) [0x00000] in <filename unknown>:0
at PriestController3D+<Floorbound>c__Iterator0.<>m__0 (System.String ) [0x00000] in <filename unknown>:0
at UnityEngine.Events.UnityAction`1[System.Object].Invoke (System.Object arg0) [0x00000] in <filename unknown>:0
at UnityEngine.Events.InvokableCall`1[System.Object].Invoke (System.Object args0) [0x00000] in <filename unknown>:0
at UnityEngine.Events.UnityEvent`1[System.Object].Invoke (System.Object arg0) [0x00000] in <filename unknown>:0
at UnityEngine.UI.InputField.SendOnSubmit () [0x00000] in <filename unknown>:0
at UnityEngine.UI.InputField.DeactivateInputField () [0x00000] in <filename unknown>:0
at UnityEngine.UI.InputField.OnUpdateSelected (UnityEngine.EventSystems.BaseEventData eventData) [0x00000] in <filename unknown>:0
at UnityEngine.EventSystems.ExecuteEvents.Execute (IUpdateSelectedHandler handler, UnityEngine.EventSystems.BaseEventData eventData) [0x00000] in <filename unknown>:0
at UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[System.Object].Invoke (System.Object handler, UnityEngine.EventSystems.BaseEventData eventData) [0x00000] in <filename unknown>:0
at UnityEngine.EventSystems.ExecuteEvents.Execute[Object] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.EventFunction`1 functor) [0x00000] in <filename unknown>:0
at UnityEngine.EventSystems.StandaloneInputModule.SendUpdateEventToSelectedObject () [0x00000] in <filename unknown>:0
at UnityEngine.EventSystems.StandaloneInputModule.Process () [0x00000] in <filename unknown>:0
at UnityEngine.EventSystems.EventSystem.Update () [0x00000] in <filename unknown>:0
UnityEngine.DebugLogHandler:Internal_LogException(Exception, Object)
UnityEngine.DebugLogHandler:LogException(Exception, Object)
UnityEngine.Logger:LogException(Exception, Object)
UnityEngine.Debug:LogException(Exception)
UnityEngine.EventSystems.ExecuteEvents:Execute(GameObject, BaseEventData, EventFunction`1)
UnityEngine.EventSystems.StandaloneInputModule:SendUpdateEventToSelectedObject()
UnityEngine.EventSystems.StandaloneInputModule:Process()
UnityEngine.EventSystems.EventSystem:Update()
(Filename: currently not available on il2cpp Line: -1)
MarySuccess() method is here, which shows in the last stack trace above before it freezes:
public void MarySuccess(string inputText = "")
{
player.maryEvent.RemoveAllListeners();
QuestionText C = jsonquestions.Mary.Ctext;
QuestionText Ccopy = C.DeepCopy();
thoughtText.color = new Color(0.55f, 0f, 0.03f); // now matches (141,0,8) colour of input text, was Color.red;
thoughtText.fontSize = 22; // default I put is 28, this text too big to fit
Ccopy.phrase[1] = Ccopy.phrase[1].Replace("with seconds", "with " + countdownTimer.TenthSecondsLeft() + " seconds").Replace("received bountiful", "received " + points + " bountiful");
fillThoughtBox = FillThoughtBox(Thoughts.player, Ccopy.phrase, Ccopy.delay);
StartCoroutine(fillThoughtBox);
StartCoroutine(player.MaryHailed());
}
Thanks any suggestions!
Answer by Bunny83 · Sep 20, 2018 at 09:16 AM
Your first error is in a coroutine you haven't shown. It should be somewhere in "FillThoughtBox" inside the "Questions" class. The second error is inside "DeepCopy" of your "CorrectText" class.
You have hardcoded a count of "4". However since you get an index out of bounds exception at least one of those two arrays have less than 4 elements. Are you 100% sure that your arrays always have 4 (or more) elements? If the data is loaded external, from user data or just serialized somewhere else you can never be sure that's the case. You should practise more defensive programming
Keep in mind that your WrongText1 and WrongText2 classes have the same issue. You usually would check (or use) the Length of the arrays before working with the arrays.
Finally there seems to be no difference between CorrectText, WrongText1 and WrongText2. This is generally a bad design. You have redundant classes which all do exactly the same and hold exactly the same (kind) of data. Even "QuestionText" is the same. You probably should get rid of the "CorrectText, WrongText1 and WrongText2" classes and just use the "QuestionText" class. Your deep copy method can be implemented in that class (however with proper range checking).
Thanks kindly for the very helpful comments, much more detail than I expected, apologies for lateness of reply, I mistakenly put first reply as answer below. I removed the identical classes and put some array checking in to the unified class, but get same array out of range errors. It was a poor mistake indeed to have identical classes left in, was trying to rush it all too much perhaps. But this array error never occurs in the Unity Editor, which is why I guessed something to do with async operation of loading JSON.
I don't fully understand what UnityWebRequest and downloadhandler.text do even after reading some of the docs. I changed order of these lines in code after checking Unity docs example, but now I also have new Web Socket connection failed errors, but the game still fails exactly same place.
It loads the text of the first JSON question, so at least part of the JSON file must be loading, then fails to load wrong answer or correct answer response text. Also, each phrase in first question is loading same time, but in Unity Editor, the delay between each phrase works correctly, Qtext.delay being another array in the JSON data.
New LoadJSONData.cs in comment 2 below:
Answer by Will_Croxford · Sep 20, 2018 at 11:24 PM
EDIT In the end, the problem was JSON had not finished loading asynchronously when referenced from another class, see separate question https://answers.unity.com/questions/1565937/webgl-json-file-loads-but-cant-reference-from-diff.html. I also mentioned somewhere here I thought part of JSON data did show in the first question. This wasn't true, this text was set in the Unity Editor Inspector.
These are incredibly helpful comments thanks and much more detail than I was expecting. I read stack traces too fast as not used to it and in rush, yes first error is in FillThoughtBox() coroutine I see.
I will do best to implement all your suggestions asap, condense the identical methods and check array size first in more defensive manner. This is first game I made in Unity, as soon as you said the three classes are same, I see this, was trying do too many things first time didn't even look properly at this bit, in order to get something up to show on my github account to be frank.
I'm still bit confused why this works in Unity Editor but not in WebGL, none of these index out of range errors appear when it runs in Unity Editor. So I was thinking it was something to do with async loading of JSON, that not all of JSON in the file had loaded yet, it's all very new to me so guessing quite a lot here, after reading Unity JSON intros in different places.
Though I see your point that hard-coding array size is dangerous and will correct, I think in this particular JSON question referenced here, there seem to indeed be 4 elements in each part of array (which I assume is why it doesnt give outofrange error in Unity Editor?). DeepCopy() method in last error is called by MarySuccess() as trace shows. These are the lines calling it:
CorrectText C = jsonquestions.Mary.Ctext;
CorrectText Ccopy = C.DeepCopy();
and this is 'Mary.Ctext' JSON content in the JSON file:
"Ctext": {
"phrase": ["Yes, grace indeed, I am sorry good lady, I'm sure you didn't eat too many doughuts!\n",
"They won't be invented for a couple of centuries anyway!\nAnyway, with seconds left, I have received bountiful blessings from the Almighty!\n",
"Good old Wikipedius!\n Where would we be without your thousands of learned scribes, scratching diligently with their quills for no reward other than self-satisfaction?\n",
"Oh, Winds Across Sunny Days, I feel these initials will move me now! I feel nervous about looking around here, maybe I can in a sneaky, Mousy kind of manner...\nBut I must shift my eye to clear my thoughts, focus on leaving this place. Aye aye, I must shift my eye...\n\n"],
"delay": [3, 3, 5, 0]
}
ie it looks like 'phrase' and 'delay' arrays above do have 4 elements in them.
The other thing in WebGL game here is that when the first question appears in UI panel, there is no delay between each part of the question, it all displays at once. But when the game is run in Unity Editor there is a delay between each part of question appearing, as there should be from this 'delay' array of float values which are delay times set in FillThoughtBox() coroutine below.
I expect you're far too busy, but in case anyone interested, FillThoughtBox() coroutine here:
public IEnumerator FillThoughtBox(Thoughts panel, string[] phrase, float[] delay, Text objectText = null)
{
// set text for whether using player thought panel (own thoughts), or third person panel (other creature's voice or thoughts), or an object in the game,
// if text to appear on game object, pass the Text component of this object's panel in this case
if (panel == Thoughts.player) { thoughtText = thoughtButtonPanel.GetComponentInChildren<Text>(); }
else if (panel == Thoughts.other) { thoughtText = thirdpersonPanel.GetComponentInChildren<Text>(); }
else if (objectText) { thoughtText = objectText; }
if (thoughtText)
{
thoughtText.text = phrase[0];
yield return new WaitForSeconds(delay[0]);
if (phrase[1] != "")
{
thoughtText.text += phrase[1];
yield return new WaitForSeconds(delay[1]);
}
if (phrase[2] != "")
{
thoughtText.text += phrase[2];
yield return new WaitForSeconds(delay[2]);
}
if (phrase[3] != "")
{
thoughtText.text += phrase[3];
yield return new WaitForSeconds(delay[3]);
}
}
}
Thanks once again anyway for the detailed reply.