I have an asset saved in my project which represents a serializable scriptable object.
Code of the object is very simple:
using UnityEngine;
using System.Collections;
public class TestScriptable : ScriptableObject {
public float gravity = .3f;
public float plinkingDelay = .1f;
public float storedExecutionDelay = .3f;
}
There is no problem changing values for this object in the inspector, and the changes do persist and survive after exiting → entering Unity.
I am trying to mimic the inspector behavior in an Editor Window. But any changes I do in the Editor Window, though reflected in the Inspector, do not persist.
Here is my two scripts which lay inside the Editor folder:
the first one (auxiliary) - this script replaces inspector fields (see image above) with button, which calls my custom EditorWindow.
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(TestScriptable))]
public class TestScriptableEditor : Editor {
public override void OnInspectorGUI() {
if (GUILayout.Button("Open TestScriptableEditor"))
TestScriptableEditorWindow.Init();
}
}
the second (with my problem) - script, where I'm trying to change my asset values:
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
public class TestScriptableEditorWindow : EditorWindow {
public static TestScriptableEditorWindow testScriptableEditorWindow;
private TestScriptable testScriptable;
[MenuItem("Window/TestTaskIceCat/TestScriptableEditor")]
public static void Init() {
// initialize window, show it, set the properties
testScriptableEditorWindow = GetWindow<TestScriptableEditorWindow>(false, "TestScriptableEditorWindow", true);
testScriptableEditorWindow.Show();
testScriptableEditorWindow.Populate();
}
// initialization of my troubled asset
void Populate() {
Object[] selection = Selection.GetFiltered(typeof(TestScriptable), SelectionMode.Assets);
if (selection.Length > 0) {
if (selection[0] == null)
return;
testScriptable = (TestScriptable)selection[0];
}
}
public void OnGUI() {
if (testScriptable == null) {
/* certain actions if my asset is null */
return;
}
// Here is my tries to change values
testScriptable.gravity = EditorGUILayout.FloatField("Gravity:", testScriptable.gravity);
testScriptable.plinkingDelay = EditorGUILayout.FloatField("Plinking Delay:", testScriptable.plinkingDelay);
testScriptable.storedExecutionDelay = EditorGUILayout.FloatField("Stored Execution Delay:", testScriptable.storedExecutionDelay);
// End of the region of change values
}
void OnSelectionChange() { Populate(); Repaint(); }
void OnEnable() { Populate(); }
void OnFocus() { Populate(); }
}
My questions is: what am I doing wrong? What could be the problem? How to fix it? Am I loading the asset wrongly in the Editor Window? Or what?
Any help/ideas would be appreciated.
Well, everything is simple and complicated, and simple again at the same time.
Despite the visual changes in the inspector - it doesn't mean that the data were actually changed. It looks like everything works fine, but......In my opinion it is shortcoming of the Unity
For correctly work you should use few things:
GUI.changed - returns true if any controls changed the value of the input data. We would be using it for changes detection.
Undo.RecordObject - records any changes done on the object after the RecordObject function. The Undo state is recorded, allowing you to revert the change using the Undo system.
EditorUtility.SetDirty (!!!the most important thing!!!) - shortly: marks target object as "dirty" and therefore requiring a save. For more information - click the link.
Now, all we need to do is to write some code at the bottom of the OnGUI() method;
if (GUI.changed) {
// writing changes of the testScriptable into Undo
Undo.RecordObject(testScriptable, "Test Scriptable Editor Modify");
// mark the testScriptable object as "dirty" and save it
EditorUtility.SetDirty(testScriptable);
}
i.e. your code will be like this:
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
public class TestScriptableEditorWindow : EditorWindow {
public static TestScriptableEditorWindow testScriptableEditorWindow;
private TestScriptable testScriptable;
[MenuItem("Window/TestTaskIceCat/TestScriptableEditor")]
public static void Init() {
// initialize window, show it, set the properties
testScriptableEditorWindow = GetWindow<TestScriptableEditorWindow>(false, "TestScriptableEditorWindow", true);
testScriptableEditorWindow.Show();
testScriptableEditorWindow.Populate();
}
// initialization of troubled asset
void Populate() {
Object[] selection = Selection.GetFiltered(typeof(TestScriptable), SelectionMode.Assets);
if (selection.Length > 0) {
if (selection[0] == null)
return;
testScriptable = (TestScriptable)selection[0];
}
}
public void OnGUI() {
if (testScriptable == null) {
/* certain actions if my asset is null */
return;
}
testScriptable.gravity = EditorGUILayout.FloatField("Gravity:", testScriptable.gravity);
testScriptable.plinkingDelay = EditorGUILayout.FloatField("Plinking Delay:", testScriptable.plinkingDelay);
testScriptable.storedExecutionDelay = EditorGUILayout.FloatField("Stored Execution Delay:", testScriptable.storedExecutionDelay);
// Magic of the data saving
if (GUI.changed) {
// writing changes of the testScriptable into Undo
Undo.RecordObject(testScriptable, "Test Scriptable Editor Modify");
// mark the testScriptable object as "dirty" and save it
EditorUtility.SetDirty(testScriptable);
}
}
void OnSelectionChange() { Populate(); Repaint(); }
void OnEnable() { Populate(); }
void OnFocus() { Populate(); }
}
That's all. It was simple and easy.
Now the complicated-simple part of the story...
SetDirty - is certainly good. But this function is due to be deprecated in versions of Unity > 5.3. And also in some versions it will be removed. When? I dont't know.
Instead of using SetDirty you could go another way:
All actions in the custom Editor or EditorWindow you should do between two calls:
serializedObject.Update()
// Here is some of your code
serializedObject.ApplyModifiedProperties()
This code contains:
serializedObject.Update() - Update serialized object's representation
serializedObject.ApplyModifiedProperties() - Apply property modifications.
serializedObject - gets the access to serialized object and gets his properties. SerializedObject is used in conjunction with:
SerializedProperty - get the properties from the serializedObject. All data will be of SerializedProperty type, e.g.
SerializedProperty myGravity = serializedObject.FindProperty("gravity");
SerializedProperty myPlinkingDelay = serializedObject.FindProperty("plinkingDelay");
...
etc.
SerializedObject.FindProperty - Find serialized property by name.
EditorGUILayout.PropertyField - Make a field for SerializedProperty.
The last four is like the SetDirty: they will mark the modified object (or/and scene) as "dirty" and create Undo states for you.
So, knowing that, we could get something like this:
using UnityEngine;
using UnityEditor;
public class TestScriptableEditorWindow : EditorWindow {
public static TestScriptableEditorWindow testScriptableEditorWindow;
private TestScriptable testScriptable;
// declaring our serializable object, that we are working on
private SerializedObject serializedObj;
[MenuItem("Window/TestTaskIceCat/TestScriptableEditor")]
public static void Init() {
testScriptableEditorWindow = GetWindow<TestScriptableEditorWindow>(false, "TestScriptableEditorWindow", true);
testScriptableEditorWindow.Show();
testScriptableEditorWindow.Populate();
}
// initialization of troubled asset
void Populate() {
Object[] selection = Selection.GetFiltered(typeof(TestScriptable), SelectionMode.Assets);
if (selection.Length > 0) {
if (selection[0] == null)
return;
testScriptable = (TestScriptable)selection[0];
// initialization of the serializedObj, that we are working on
serializedObj = new SerializedObject(testScriptable);
}
}
// our manipulation
public void OnGUI() {
if (testScriptable == null) {
/* certain actions if my asset is null */
return;
}
// Starting our manipulation
// We're doing this before property rendering
serializedObj.Update();
// Gets the property of our asset and скуфеу a field with its value
EditorGUILayout.PropertyField(serializedObj.FindProperty("gravity"), new GUIContent("Gravity"), true);
EditorGUILayout.PropertyField(serializedObj.FindProperty("plinkingDelay"), new GUIContent("Plinking Delay"), true);
EditorGUILayout.PropertyField(serializedObj.FindProperty("storedExecutionDelay"), new GUIContent("Stored Execution Delay"), true);
// Apply changes
serializedObj.ApplyModifiedProperties();
}
void OnSelectionChange() { Populate(); Repaint(); }
void OnEnable() { Populate(); }
void OnFocus() { Populate(); }
}
So, it is simple because you should use just
Update → actions → ApplyModifiedProperties.
But it is complicated because you should do a lot of work with bunch of property classes: FindProperty, PropertyField и SerializedProperty.
But when you understand how it works - it is becoming so easy...
Related
For example, there's two classes here.
Class One:
using UnityEngine;
using System.Collections.Generic;
public class One:MonoBehavior
{
private List<string> _assetBundleList;
private void Start()
{
InitializeList();
}
private void InitializeList()
{
//Add assetBundleList field some value.
}
public IEnumerator<string> GetEnumerator()
{
return _assetBundleList.GetEnumerator();
}
}
Class two:
public class Two:MonoBehavior
{
public GameObject gameObjectWithScriptOne;
private One _scriptOne;
private void Start()
{
scriptOne = gameObjectWithScriptOne.GetComponent<One>();
DoSomething();
}
private void DoSomething()
{
foreach(var assetBundle in scriptOne)
{
//Load asset
}
}
}
Script one is just like a manager things, I use this for storing asset bundle data, perhaps the list value will change. Script two must wait for initializing done. Is there any way to wait for it except adjusting script order?
It can be handled with easily creating a context that initialize things sequentially. In short you need to block the main thread.
Let's say you have a root or persistent scene that do must things like loading assets etc.
//The only object in the Root Scene.
public class RootContext : MonoBehaviour
{
private const string _playerAddress = "Player";
public void Awake()
{
//Load assets
//wait for assets loading
//instantiate things.
//new instantiated things can also initalize with this way.
var handle = Addressables.LoadAssetAsync<GameObject>(_playerAddress);
var asset = handle.WaitForCompletion();
var go = Instantiate(bla bla bla);
}
}
I think the what you want is a framework that organize things. You can look at Strange. It is MVCS + IoC framework. Addionality what you said about "when field or property initialization is done" with [PostConstruct] attribute with Strange.
[Inject(ContextKeys.CONTEXT)]
public IContext Context { get; set; }
/*Initialization of injections are guaranteed at here. Their initialization
is also quaranteed like this class.
*/
[PostConstruct]
public void Initalize()
{
}
I'd go with refactoring, but if this is not the case C# events might be the solution.
Class One
public class One:MonoBehavior
{
private List<string> _assetBundleList;
public event Func Initialized;
private void Start()
{
InitializeList();
}
private void InitializeList()
{
//Add assetBundleList field some value.
Initialized?.Invoke();
}
public IEnumerator<string> GetEnumerator()
{
return _assetBundleList.GetEnumerator();
}
}
Class Two:
public class Two:MonoBehavior
{
public GameObject gameObjectWithScriptOne;
private One _scriptOne;
private void Start()
{
scriptOne = gameObjectWithScriptOne.GetComponent<One>();
scriptOne.Initialized += DoSomething;
}
private void DoSomething()
{
foreach(var assetBundle in scriptOne)
{
//Load asset
}
}
}
Also, you should unsubscribe from events when disabling an object, but you'll figure it out by yourself.
I was using PlayFab and decided to migrate to Firebase.
I want to use FirebaseAuth to implement a login system in my Unity game (and removed the PlayFab code).
So I made a FirebaseManager.cs class, and modified the Script Execution Order to make it run before any other custom script.
I tried to implement what is necessary to create an account and it works great.
I know Firebase silently keeps the user connected between sessions. So I want to check wether a user is already connected or not, from my "home scene" (which is also the scene where you login/create account).
public class FirebaseManager : MonoBehaviour
{
private static FirebaseManager _instance;
public static FirebaseManager Instance { ... }
private FirebaseApp _app;
private FirebaseAuth _auth;
public FirebaseAuth Auth
{
get
{
if (_auth == null)
{
_auth = FirebaseAuth.GetAuth(_app);
}
return _auth;
}
}
private FirebaseUser _user;
public FirebaseUser User
{
get
{
return _user;
}
set
{
_user = value;
}
}
// Events to subscribe to for this service
public delegate void FirebaseInitialized();
public static event FirebaseInitialized OnFirebaseInitialized;
private void Awake()
{
if (_instance == null)
{
DontDestroyOnLoad(this.gameObject);
_instance = this;
InitializeFirebase();
}
else { Destroy(this.gameObject); }
}
private void InitializeFirebase()
{
Debug.Log($"FirebaseManager.cs > INITIALIZING FIREBASE");
Firebase.FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(task =>
{
var dependencyStatus = task.Result;
if (dependencyStatus == Firebase.DependencyStatus.Available)
{
// Create and hold a reference to your FirebaseApp,
// where app is a Firebase.FirebaseApp property of your application class.
_app = FirebaseApp.DefaultInstance;
// Create and hold a reference to FirebaseAuth
_auth = FirebaseAuth.DefaultInstance;
// Create and hold a reference to already logged in user (if any)
_user = _auth.CurrentUser;
if (OnFirebaseInitialized != null) OnFirebaseInitialized.Invoke();
}
else
{
UnityEngine.Debug.LogError(System.String.Format(
"Could not resolve all Firebase dependencies: {0}", dependencyStatus));
// Firebase Unity SDK is not safe to use here.
}
});
}
private void OnDestroy()
{
if (_auth != null) _auth = null;
}
}
I also made a static AuthService.cs class to call from my scene :
public static class AuthService
{
private static FirebaseAuth auth = FirebaseManager.Instance.Auth;
public static bool IsUserAlreadyLoggedIn()
{
Debug.Log($"AuthService.cs > Checking if user already logged in");
return auth.CurrentUser != null;
}
}
Then in my Login Scene, I have this LoginManager.cs class :
public class LoginManager : MonoBehaviour
{
// Loading Animation
[Header("Loading Animation")]
public GameObject loadingAnimation;
private void Awake()
{
FirebaseManager.OnFirebaseInitialized += OnFirebaseInitialized;
}
private void Start()
{
ShowLoadingAnimation(true);
}
private void OnFirebaseInitialized()
{
if (AuthService.IsUserAlreadyLoggedIn())
{
Debug.Log($"Already LoggedIn");
OnLoginSuccess();
}
else
{
Debug.Log($"No User LoggedIn");
ShowLoadingAnimation(false);
}
}
private void OnLoginSuccess()
{
ShowLoadingAnimation(false); // From here, no code is executed
Debug.Log($"Logged In as: {FirebaseManager.Instance.Auth.CurrentUser.Email}");
// SceneLoader.Instance.Load(SceneLoader.Scene.CharacterCreation);
}
private void OnDestroy()
{
FirebaseManager.OnFirebaseInitialized -= OnFirebaseInitialized;
}
/// <summary>
/// Show or Hide the Loading Animation
/// </summary>
/// <param name="show">True = show || False = hide</param>
private void ShowLoadingAnimation(bool show)
{
loadingAnimation.SetActive(show);
}
}
The Loading animation gameObject is just an animated image (+ another image which is used to block user input).
Whenever the code reaches the ShowLoadingAnimation() method. All code after it is NOT executed. But if I remove all the calls to ShowLoadingAnimation(), everything works fine.
The same logic was working fine with PlayFab.
I just don't understand what is happening. Maybe something to do with threads? Or maybe just my Firebase implementation is wrong? What can I do?
Thanks.
One explanation would be that OnFirebaseInitialized gets called on a background thread.
Unity's MonoBehaviour system isn't thread safe, so if a GameObject is set inactive from a background thread this could result in exceptions and in code execution coming to a stop.
To avoid the issue you could have OnLoginSuccess just set a boolean variable to true, and then inside the Update method (which gets called on the main thread) check when this variable is true and call ShowLoadingAnimation(false).
You should also only access this boolean variable inside a lock statement to ensure thread safety.
So I've found a solution. As Sisus said (thanks for your answer, it helped me in my Google searches), this issue is caused by the code running on another thread instead of the main one.
According to this Firebase video, when initializing Firebase, I can call ContinueWithOnMainThread instead of simply ContinueWith to solve that issue.
(Note that this require using Firebase.Extensions, see Firebase.Extensions.TaskExtension docs)
So updated Firebase initialization looks like this:
Firebase.FirebaseApp.CheckAndFixDependenciesAsync().ContinueWithOnMainThread(task =>
{
var dependencyStatus = task.Result;
if (dependencyStatus == Firebase.DependencyStatus.Available)
{
// Create and hold a reference to your FirebaseApp,
// where app is a Firebase.FirebaseApp property of your application class.
_app = FirebaseApp.DefaultInstance;
// Create and hold a reference to FirebaseAuth
_auth = FirebaseAuth.DefaultInstance;
// Create and hold a reference to already logged in user (if any)
_user = _auth.CurrentUser;
if (OnFirebaseInitialized != null) OnFirebaseInitialized.Invoke();
}
else
{
UnityEngine.Debug.LogError(System.String.Format(
"Could not resolve all Firebase dependencies: {0}", dependencyStatus));
// Firebase Unity SDK is not safe to use here.
}
});
The Idea is, that my Data get loaded, asynchron, when my app startet. Because the user normaly need some seconds to interact and in this time, i should get my data. (For understanding reasons, it will be a book app, with some books)
To do this, in loadingprofile i open a method on a static class to start loading.
In the Screen where i want to show my data, i check with a method on the static class, if the data are there ( and wait if theire not).
So in the Profil loading screen i did this:
public class LoadProfiles : MonoBehaviour
{
private void Awake()
{
if (!LoadingData.dataLoaded)
{
LoadingData.LoadData();
}
}
void Start()
{
//Loading Profile Dataa
}
}
Then when the User click on a profile, i open the next scene "Bookshelf" and a game object in it has the following script attachted to read the data (the book categories). so i open the wait on data method, that shoud return, when the data are arrived.
public class PopulateShelf : MonoBehaviour
{
private void Start()
{
LoadingData.WaitOnData();
LoadingData.Categories.ForEach(c => {
//create categories
});
}
}
The static loading class looks like the following
public class LoadingData : MonoBehaviour
{
public static List<CategoryDTO> Categories;
public static IEnumerator CategoriesRequest;
public static bool dataLoaded = false;
public static void LoadData()
{
CategoriesRequest = LoadCategories();
dataLoaded = true;
}
public static void WaitOnData()
{
if (!dataLoaded) LoadData();
while (CategoriesRequest.MoveNext())
{
//Log wait on categories
}
}
public static IEnumerator LoadCategories()
{
UnityWebRequest request = UnityWebRequest.Get(Constants.apiURL + Constants.apiURLCategories);
yield return request.SendWebRequest();
while (!request.isDone)
{
yield return true;
}
if (request.result == UnityWebRequest.Result.ConnectionError)
{
//Log error
}
else
{
Categories = JsonConvert.DeserializeObject<List<CategoryDTO>>(request.downloadHandler.text);
Categories.Sort((x, y) => x.Order.CompareTo(y.Order));
}
}
}
So, the problem is that the on the loging screen, it opens the LoadingData.LoadData Method (but it do not start loading).
When im in the Bookshelf Scene, and open LoadingData.WaitOnData it starts loading the data and the User have to wait.
I also tried it with StartCoroutines. I refactored the LoadingData class, to a non static class. Startet the Coroutings in the LoadData Method.
First it looked like it worked, the data was loaded in the profileScene
Because of it was not static, i had to pass somehow the Object. To do this i created in a static script a Constants for a static LoadingData script variable.
In the Profile Scene i attached it and loaded the data. When i wanted to read it in the BookShefScene, i got a null refrence in my static class...
Here would be the class with coroutines
public class LoadingData : MonoBehaviour
{
public Dictionary<int, BookDTO> Books = new Dictionary<int, BookDTO>();
public List<CategoryDTO> Categories;
public bool dataLoaded = false;
public void LoadData()
{
StartCoroutine(LoadCategories());
StartCoroutine(LoadBooks());
dataLoaded = true;
}
public void WaitOnData()
{
if (!dataLoaded) LoadData();
while (categorieLoaded)
{
//log waiting
}
}
public IEnumerator LoadCategories()
{
UnityWebRequest request = UnityWebRequest.Get(Constants.apiURL + Constants.apiURLCategories);
yield return request.SendWebRequest();
while (!request.isDone)
{
yield return true;
}
if (request.result == UnityWebRequest.Result.ConnectionError)
{
//Log Error
}
else
{
Categories = JsonConvert.DeserializeObject<List<CategoryDTO>>(request.downloadHandler.text);
Categories.Sort((x, y) => x.Order.CompareTo(y.Order));
}
}
}
in profile loading screen, to attache my script to the static variable, i said something like this:
Constants.loadingData = this.GetComponent<LoadingData>();
Constants.loadingData.LoadData();
and in the bookshelf i said
Constants.loadingData.WaitOnData();
Constants.loadingData.Categories();
I checked with the Debuger, in the Profile Scene the Constants.loadingData was created and referenced.
In the Bookshelf Scene, i dont saw the referenced object (it was null)...
can anyoune help me with one of this solution? or have a third solution?
I think your condition should be
if(!dataLoaded) LoadData();
Same for the categorieLoaded, maybe the name is misleading and you are using them correctly (?)
Is the GameObject stored in Constants.loadedData set as dontDestroyOnLoad? Maybe when you change scene it gets destroied and set to null.
Also I would suggest to have a better way of waiting for the data to load, if it takes seconds the user will not be able to interract with the game since the while loop will freeze the entire window, you can make your own method that checks every frame if the data got loaded.
private IEnumerable WaitForData()
{
while(!Constants.loadedData.Loaded) return null;
// populate the bookshelf
// foreach(Book book in Constants.Books)
}
You need to call LoadData() somewhere before calling this, you could setup a loading screen while waiting aswell to make players understand.
I am very new to C# and try to use Microsoft.MixedReality.QR for reading some QRCodes with the Hololense 2 and have problems accessing the events of the QRCodeWatcher. According to the API, the QRCodeWatcher class has 4 events but it seems that these are never triggered. (QR_Added, QR_Updated,... are never called). Am I setting up something wrong?
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Microsoft.MixedReality.QR;
using QRTracking;
namespace QRTracking {
public class Detector: MonoBehaviour {
public TextMesh debugScreen;
public bool IsSupported {get; private set; }
private QRCodeWatcher qrTracker;
private bool QRstarted=false;
private QRCodeWatcherAccessStatus accessStatus;
// Start is called before the first frame update
async protected virtual void Start() {
debugScreen.text="Hello";
if (QRCodeWatcher.IsSupported()) {
try {
accessStatus=await QRCodeWatcher.RequestAccessAsync();
debugScreen.text=accessStatus.ToString();
}
catch {
debugScreen.text="No World";
}
}
}
// Update is called once per frame
void Update() {
if (accessStatus.ToString()=="Allowed") {
if (QRstarted==false) {
debugScreen.text="Setup Tracking";
SetupQRTracking();
QRstarted=true;
}
}
}
private void SetupQRTracking() {
qrTracker=new QRCodeWatcher();
qrTracker.Added+=QR_Added;
qrTracker.Updated+=QR_Updated;
qrTracker.Removed+=QR_Removed;
qrTracker.EnumerationCompleted+=QR_Enum;
qrTracker.Start();
debugScreen.text="Tracking started";
}
private void QR_Added(object sender, QRCodeAddedEventArgs args) {
debugScreen.text="Added";
}
private void QR_Updated(object sender, QRCodeUpdatedEventArgs args) {
debugScreen.text="Updated";
}
private void QR_Removed(object sender, QRCodeRemovedEventArgs args) {
debugScreen.text="Removed";
}
private void QR_Enum(object sender, object e) {
debugScreen.text="Enum";
}
}
}
(While the script is running the HoloLense is able to read the content of a QRCode. The debugScreen is a TextMesh so that I can see the text while wearing the HoloLense.)
Workaround to get only the recently seen QR codes to mimic the update event:
private IReadOnlyList<QRCode> detectedQR;
private DateTimeOffset timestamp;
private int timetolerance = 1; // seconds
detectedQR = qrTracker.GetList();
timestamp = DateTimeOffset.Now;
foreach (var QR in detectedQR)
{if ((timestamp - QR.LastDetectedTime ).Duration() < TimeSpan.FromSeconds(timetolerance))
{
//Do stuff with QR Codes seen in the last second
}
}
However, with too many QR codes, performance could suffer.
Hi I’m Wayne Wang from the Microsoft for Founders Hub team!
We do have an example from my colleague on your current context.
Seems the implementation you may need is here:
https://github.com/chgatla-microsoft/QRTracking/blob/master/SampleQRCodes/Assets/Scripts/QRCodesManager.cs
it was little bit of old but I believe the Interfaces and pipelines still the same
I am making a basic shop system includes car name, price and buyable. I am saving data to a text file. When I replay, I can't get the last variables I've changed. But, if I refresh at Editor by CTRL+R and start the game, variables are loading correct. What am I doing wrong? I am new at storage and JSON issues. Thanks for answers...
Here is the code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System;
public class GameHandler : MonoBehaviour
{
[Serializable]
public class Car
{
public string name;
public int price;
public bool unlocked;
}
[Serializable]
public class CarList{
public Car[] cars;
}
private Car cars = new Car();
public CarList myCars = new CarList();
//-------
public int chosenCar;
//-------
public TextAsset CARS;
//-------
private void Start() {
GetFromJSON();
}
private void Update() {
if(Input.GetKeyDown(KeyCode.L))
GetFromJSON();
if(Input.GetKeyDown(KeyCode.S))
SaveToJSON();
}
public void ChangeCarIndex(int a){
chosenCar = a;
}
//This is a button func.
public void ChangePrice(int a){
myCars.cars[chosenCar].price = a;
SaveToJSON();
}
public void GetCarName(){
Debug.Log(myCars.cars[chosenCar].name);
}
public void GetCarPrice(){
Debug.Log(myCars.cars[chosenCar].price);
}
public void SaveToJSON(){
string jsOutput = JsonUtility.ToJson(myCars);
File.WriteAllText(Application.dataPath + "/CARS.txt", jsOutput);
Debug.Log("SaveToJSON() called");
}
public void GetFromJSON(){
myCars = JsonUtility.FromJson<CarList>(CARS.text);
Debug.Log("GetFromJSON() called");
}
}
When running this in the editor try to add
public void SaveToJSON()
{
string jsOutput = JsonUtility.ToJson(myCars);
File.WriteAllText(Application.dataPath + "/CARS.txt", jsOutput);
Debug.Log("SaveToJSON() called");
#if UNITY_EDITOR
UnityEditor.AssetDatabase.Refresh();
#endif
}
Though actually this isn't really necessary! Afaik you could also simply set the text of the textasset:
public void SaveToJSON()
{
string jsOutput = JsonUtility.ToJson(myCars);
CARS.text = jsOutput;
Debug.Log("SaveToJSON() called");
}
BUT NOTE:
in general note that this makes only sense in the editor itself.
If you target to do this in an actually later built application you would rather go via a file in Application.persistentDataPath. Problem with that though: The data can easily be seen and edited by the user. So if this is anything sensitive you will need to go for a central database server with user login.