I try to achieve a tiny custom editor in Unity3D which offers buttons to modify a value in two components of the same game object. So in the "PlanetGameObject" there is the MonoBehaviour "Planet" where the member "phase" should change (to one of the matching enum values) and also a SpriteRenderer where the member "color" changes to the matching value of the phase.
Basically it works as intended. When I click the buttons in the editor both values get assigned correctly but when the game starts the phase switches back to the initial value while the color of the sprite renderer stays with the new value. When I close the running game the incorrect value stays.
The setup:
Planet script:
using UnityEngine;
public class Planet : MonoBehaviour
{
public enum Phase { NEUTRAL, RED, GREEN, BLUE }
public Phase phase = Phase.NEUTRAL;
public static Color PhaseColor(Phase phase)
{
switch (phase)
{
case Phase.RED: return Color.red;
case Phase.GREEN: return Color.green;
case Phase.BLUE: return Color.blue;
default: return Color.white;
}
}
}
The custom editor script:
[![using UnityEngine;
using UnityEditor;
\[CustomEditor(typeof(Planet))\]
\[CanEditMultipleObjects\]
public class PlanetEditor : Editor
{
public override void OnInspectorGUI()
{
GUILayout.BeginHorizontal();
if (GUILayout.Button("neutral"))
{
foreach(Object t in targets)
{
Planet planet = (Planet)t;
planet.phase = Planet.Phase.NEUTRAL;
planet.gameObject.GetComponent<SpriteRenderer>().color = Planet.PhaseColor(planet.phase);
}
};
if (GUILayout.Button("red"))
{
foreach (Object t in targets)
{
Planet planet = (Planet)t;
planet.phase = Planet.Phase.RED;
planet.gameObject.GetComponent<SpriteRenderer>().color = Planet.PhaseColor(planet.phase);
}
};
if (GUILayout.Button("green"))
{
foreach (Object t in targets)
{
Planet planet = (Planet)t;
planet.phase = Planet.Phase.GREEN;
planet.gameObject.GetComponent<SpriteRenderer>().color = Planet.PhaseColor(planet.phase);
}
};
if (GUILayout.Button("blue"))
{
foreach (Object t in targets)
{
Planet planet = (Planet)t;
planet.phase = Planet.Phase.BLUE;
planet.gameObject.GetComponent<SpriteRenderer>().color = Planet.PhaseColor(planet.phase);
}
};
GUILayout.EndHorizontal();
DrawDefaultInspector();
}
}][1]][1]
Never go through the actual component references in Editor scripts.
This won't work with saving and undo/redo etc. because it doesn't mark the changes as dirty automatically.
Rather go through the SerializedPropertys which automatically handles marking stuff as dirty => saving. And Undo/Redo etc.
Could look somewhat like
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(Planet))]
[CanEditMultipleObjects]
public class PlanetEditor : Editor
{
SerializedProperty _phase;
private void OnEnable()
{
// This links the _phase SerializedProperty to the according actual field
_phase = serializedObject.FindProperty("phase");
}
public override void OnInspectorGUI()
{
// This gets the current values from all serialized fields into the serialized "clone"
serilaizedObject.Update();
GUILayout.BeginHorizontal();
if (GUILayout.Button("neutral"))
{
// It is enough to do this without a loop as this will already affect all selected instances
phase.intValue = (int)Planet.Phase.NEUTRAL;
};
if (GUILayout.Button("red"))
{
phase.intValue = (int)Planet.Phase.RED;
};
if (GUILayout.Button("green"))
{
phase.intValue = (int)Planet.Phase.GREEN;
};
if (GUILayout.Button("blue"))
{
phase.intValue = (int)Planet.Phase.RED;
};
GUILayout.EndHorizontal();
// This writes the changed properties back into the actual instance(s)
serializedObject.ApplyModifiedProperties();
DrawDefaultInspector();
}
}
So far for saving the enum of this component itself.
Now the simpliest way for the renderer would be to let your class itself react to that change:
[RequireComponent(typeof(SpriteRenderer))]
public class Planet : MonoBehaviour
{
public enum Phase { NEUTRAL, RED, GREEN, BLUE }
public Phase phase = Phase.NEUTRAL;
// Will be called everytime a field in the Inspector is changed
private void OnValidate()
{
GetComponent<SpriteRenderer>().color = PhaseColor(phase);
}
public static Color PhaseColor(Phase phase)
{
switch (phase)
{
case Phase.RED: return Color.red;
case Phase.GREEN: return Color.green;
case Phase.BLUE: return Color.blue;
default: return Color.white;
}
}
}
As this also should handle the marking dirty and thereby saving correctly afaik.
Actually your editor becomes a bit redundant with this since this also already reacts if you change the enum field manually ;)
Note: Typed on smartphone but I hope the idea gets clear
Related
I'm trying to access a boolean from another script, which I using a custom namespace. The idea is to have a checkbox in the inspector per item if it should be animated or not. On a weaponmesh, I have a materialoffset animator that should trigger if that box is checked.
using MFPSEditor;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MFPS.Internal.Structures;
namespace MFPS.Addon.Customizer
{
At the bottom of this script, I have this:
[System.Serializable]
public class GlobalCamo
{
public string Name;
[SpritePreview(50)] public Texture2D Preview;
public MFPSItemUnlockability Unlockability;
public string Description;
public bool isAnimated;
public int Price
{
get => Unlockability.Price;
}
public bool animatedCamo()
{
return isAnimated;
}
public bool isFree() { return Price <= 0; }
[HideInInspector] public Sprite m_sprite = null;
public Sprite spritePreview()
{
if (m_sprite == null && Preview != null)
{
m_sprite = Sprite.Create(Preview, new Rect(0, 0, Preview.width, Preview.height), new Vector2(0.5f, 0.5f));
}
return m_sprite;
}
}
}
I am trying to access the variable "isAnimated" in the script below:
using UnityEngine;
public class MaterialOffsetAnimator : MonoBehaviour
{
// The material that will be animated
Material material;
// The speed at which the offset will be animated
public Vector2 animationSpeed;
void Update()
{
material = GetComponent<Renderer>().material;
if ( isAnimated == true )
{
// Update the material's offset based on the animation speed
material.mainTextureOffset += animationSpeed * Time.deltaTime;
}
else
{
}
}
}
isAnimated is an instance field in your GlobalCamo class, not a static field. You can't access it without a reference to an instance of GlobalCamo.
You can just make the isAnimated field static, like this:
public static bool isAnimated;
And then access it in MaterialOffsetAnimator like this:
if (GlobalCamo.isAnimated)
{
// Your code
}
Also, a side note: You don't need to use == true with booleans. If a boolean is true, the condition will evaluate to true.
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.
GameContainer script:
public class GameContainer : MonoBehaviour
{
public List<Game> Games;
public void AddGame()
{
Games.Add(new Game());
}
}
Game Class:
[System.Serializable]
public class Game
{
public List<GameType> gameTypes;
public void addGameType()
{
gameTypes.Add(new GameType());
}
}
GameType Class
[System.Serializable]
public class GameType
{
}
and my OnInspectorGUI method in custom editor
public override void OnInspectorGUI()
{
var targetScript = target as GameContainer;
var centeredStyle = GUI.skin.GetStyle("Label");
centeredStyle.alignment = TextAnchor.UpperCenter;
centeredStyle.fontStyle = FontStyle.Bold;
EditorGUILayout.LabelField("Games", centeredStyle);
for(int i = 0;i<targetScript.Games.Count; i++)
{
Game game = targetScript.Games[i];
//here is the LINE CAUSING A PROBLEM
Debug.Log(game.gameTypes.Count);
GUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.Space();
GUILayout.BeginVertical("Game Types", "window");
if (GUILayout.Button("+"))
{
game.addGameType();
}
GUILayout.EndVertical();
GUILayout.EndVertical();
EditorGUILayout.Space();
}
if (GUILayout.Button("+"))
{
targetScript.AddGame();
}
}
the problem is with this line:
//here is the LINE CAUSING A PROBLEM
Debug.Log(game.gameTypes.Count);
when i hit AddGame Button, all draw calls after this line will be ignored for newly added element and its not shown till next change in code and refresh in the editor, if i remove this line, everything works just fine.
but if i try to use gameType list by any mean, it will not show correct view in inspector.
what the problem is?
I recommend using EditorGUILayout instead of old GUILayout class.
here's link to document for it:
https://docs.unity3d.com/ScriptReference/EditorGUILayout.html
Although unity introduced a new way to make custom editors lately that is called UI Elements.
You can create your own editors with layered architecture with xml,css like language.
here's some useful YouTube links for you:
https://www.youtube.com/watch?v=sVEmJ5-dr5E
https://www.youtube.com/watch?v=MNNURw0LeoQ
https://www.youtube.com/watch?v=GSRVI1HqlF4
And lastly you can check this beautiful editor attributes too:
https://github.com/dbrizov/NaughtyAttributes
I have been working on a dialogue system for my game and I was wondering if anyone knows how to keep the system between different scenes. I know you can use things such as Player Prefs but for one, I do not understand it and upon research, people do not generally recommend it for storing large complicated things. I managed to get close to doing so by using dontDestroy just as you would with a character, however, it did not work completely as the button to switch to the next line of text, of course, broke along with the singleton I created for my system. What would be the best way for me to go about this?
Here is all of my code just in case it is needed:
Making the scriptable object:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "New Dialogue", menuName = "Dialogues")]
public class Dialogue : ScriptableObject
{
[System.Serializable]
public class Info
{
public string myName;
public Sprite portrait;
[TextArea(4, 8)]
public string mytext;
}
[Header("Insert Dialogue Info Below")]
public Info[] dialogueInfoSection;
}
Main code for system (sigleton breaks here while switching scenes):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class MainDialogueManager : MonoBehaviour
{
public static MainDialogueManager instance;
private void Awake()
{
if(instance != null)
{
Debug.LogWarning("FIX THIS" + gameObject.name);
}
else
{
instance = this;
}
}
public GameObject DialogueBoX;
public Text dialogueNameofChar;
public Text characterSays;
public Image characterPortrait;
private float textDelay = 0.005f;
public Queue<Dialogue.Info> dialogueInfoSection = new Queue<Dialogue.Info>();
public void EnqueueDialogue(Dialogue db)
{
DialogueBoX.SetActive(true);
dialogueInfoSection.Clear();
foreach(Dialogue.Info info in db.dialogueInfoSection)
{
dialogueInfoSection.Enqueue(info);
}
DequeueDialogue();
}
public void DequeueDialogue()
{
if (dialogueInfoSection.Count==0)
{
ReachedEndOfDialogue();
return; /////
}
Dialogue.Info info = dialogueInfoSection.Dequeue();
dialogueNameofChar.text = info.myName;
characterSays.text = info.mytext;
characterPortrait.sprite = info.portrait;
StartCoroutine(TypeText(info));
}
IEnumerator TypeText(Dialogue.Info info)
{
characterSays.text= "";
foreach(char c in info.mytext.ToCharArray())
{
yield return new WaitForSeconds(textDelay);
characterSays.text += c;
yield return null;
}
}
public void ReachedEndOfDialogue()
{
DialogueBoX.SetActive(false);
}
}
Dialogue Activation:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MainDialogueActivation : MonoBehaviour
{
public Dialogue dialogue;
public void startActivationofDialogue()
{
MainDialogueManager.instance.EnqueueDialogue(dialogue);
}
private void Start()
{
startActivationofDialogue();
}
}
Go to next dialogue line:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MainDialogueButtons : MonoBehaviour
{
public void GoToNextDialogueLine()
{
MainDialogueManager.instance.DequeueDialogue();
}
}
How about something like this?
The idea is pretty similar to what you're doing, with a few tweaks:
I'm storing the active dialog in a scriptable object (DialogueSystem) so that it can persist between scenes. Each time I load a new scene, I check if there's an active dialog, and if I so I show the dialog popup in Start().
Whereas you remove the dialog section that you're currently showing to the player from the current dialog, I don't remove the current section until the player clicks to the next section. That's necessary because you may need to re-show the same section if you move to a new scene.
Make sure to create an instance of the DialogueSystem scriptable object and assign it to MainDialogueActivation and MainDialogManager
MainDialogActiviation has some testing code in it so you can hit a key to start a new dialog or switch between scenes.
MainDialogueActiviation.cs
using UnityEngine;
using UnityEngine.SceneManagement;
public class MainDialogueActivation : MonoBehaviour
{
public Dialogue dialogue;
// This scriptable object stores the active dialog so that you
// can persist it between scenes
public DialogueSystem dialogSystem;
private void Start()
{
// If we had an active dialog from the previous scene, resume that dialog
if (dialogSystem?.dialogInfoSections.Count > 0)
{
GetComponent<MainDialogueManager>().ShowDialog();
}
}
private void Update()
{
// Pressing D queues and shows a new dialog
if (Input.GetKeyDown(KeyCode.D))
{
GetComponent<MainDialogueManager>().EnqueueDialogue(this.dialogue);
}
// Pressing C ends the current dialog
if (Input.GetKeyDown(KeyCode.C))
{
this.dialogSystem.dialogInfoSections.Clear();
GetComponent<MainDialogueManager>().ReachedEndOfDialogue();
}
// Pressing S swaps between two scenes so you can see the dialog
// persisting
if (Input.GetKeyDown(KeyCode.S))
{
if (SceneManager.GetActiveScene().name == "Scene 1")
{
SceneManager.LoadScene("Scene 2");
}
else if (SceneManager.GetActiveScene().name == "Scene 2")
{
SceneManager.LoadScene("Scene 1");
}
}
}
}
MainDialogueManager.cs
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class MainDialogueManager : MonoBehaviour
{
// This scriptable object stores the active dialog
public DialogueSystem dialogSystem;
public GameObject DialogueBox;
public Text dialogueNameofChar;
public Text characterSays;
public Image characterPortrait;
private float textDelay = 0.005f;
// The game object for the dialog box that is instantiated in this
// scene
private GameObject dialogBoxGameObject;
/// <summary>
/// Shows the dialog window for the dialog that is in this object's
/// dialogSystem property.
/// </summary>
public void ShowDialog()
{
// Instantiate the dialog box prefab
this.dialogBoxGameObject = Instantiate(this.DialogueBox);
// I'd recommend putting a script on your "dialog box" prefab to
// handle this stuff, so that this script doesn't need to get a
// reference to each text element within the dialog prefab. But
// this is just a quick and dirty example for this answer
this.dialogueNameofChar = GameObject.Find("Character Name").GetComponent<Text>();
this.characterSays = GameObject.Find("Character Text").GetComponent<Text>();
this.characterPortrait = GameObject.Find("Character Image").GetComponent<Image>();
// If you have multiple response options, you'd wire them up here.
// Again; I recommend putting this into a script on your dialog box
GameObject.Find("Response Button 1").GetComponent<Button>().onClick.AddListener(ShowNextDialogSection);
GameObject.Find("Response Button 2").GetComponent<Button>().onClick.AddListener(ShowNextDialogSection);
ShowDialogSection(this.dialogSystem.dialogInfoSections.Peek());
}
/// <summary>
/// Puts a dialog into this object's dialogSystem property and
/// opens a dialog window that will show that dialog.
/// </summary>
public void EnqueueDialogue(Dialogue db)
{
foreach (Dialogue.Info info in db.dialogueInfoSection)
{
this.dialogSystem.dialogInfoSections.Enqueue(info);
}
ShowDialog();
}
/// <summary>
/// Removes the dialog section at the head of the dialog queue,
/// and shows the following dialog statement to the player. This
/// is a difference in the overall logic, because now the dialog
/// section at the head of the queue is the dialog that's currently
/// being show, rather than the previous one that was shown
/// </summary>
public void ShowNextDialogSection()
{
this.dialogSystem.dialogInfoSections.Dequeue();
if (this.dialogSystem.dialogInfoSections.Count == 0)
{
ReachedEndOfDialogue();
return;
}
Dialogue.Info dialogSection = this.dialogSystem.dialogInfoSections.Peek();
ShowDialogSection(dialogSection);
}
/// <summary>
/// Shows the specified dialog statement to the player.
/// </summary>
public void ShowDialogSection(Dialogue.Info dialogSection)
{
dialogueNameofChar.text = dialogSection.myName;
characterSays.text = dialogSection.mytext;
characterPortrait.sprite = dialogSection.portrait;
StartCoroutine(TypeText(dialogSection));
}
IEnumerator TypeText(Dialogue.Info info)
{
characterSays.text = "";
foreach (char c in info.mytext.ToCharArray())
{
yield return new WaitForSeconds(textDelay);
characterSays.text += c;
yield return null;
}
}
public void ReachedEndOfDialogue()
{
// Destroy the dialog box
Destroy(this.dialogBoxGameObject);
}
}
DialogSystem.cs
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(menuName = "Dialogues/Dialog System")]
public class DialogueSystem : ScriptableObject
{
public Queue<Dialogue.Info> dialogInfoSections = new Queue<Dialogue.Info>();
}
Here's what my dialog box prefab looks like
Every scene needs an object (presumably a prefab to make it easy to add to every scene) that has MainDialogActiviation and MainDialogManager on it. Mine looks like this:
This might be a bit of an unpopular opinion but using Singleton's are fine. It's just that MonoBehaviour singletons are tricky, you can use Object.DontDestroyOnLoad(instance). But things get ugly because it doesn't get destroyed when the scene changes (good) but if you go back to the scene it will load another one (bad). There's a few ways to get around that like having the object destroy itself if there's already an instance or having a subscene.
I would suggest not using MonoBehaviour singletons and use ScriptableObject singletons. You can lazy instantiate by putting the asset in a resource folder and use Resource.Load like this.
public class ScriptableSingleton<T> : ScriptableObject where T : ScriptableSingleton<T> {
private static string ResourcePath {
get {
return typeof(T).Name;
}
}
public static T Instance {
get {
if (instance == null) {
instance = Resources.Load(ResourcePath) as T;
}
return instance;
}
}
private static T instance;
}
With this code you create a Singleton class say DialogueManager you create a DialogueManager.asset for it and put it in a "Resources" folder.
I'm just asking if there is any possibility to hide the "Object Picker" (The little knob/menu next to an ObjectField) in a custom Inspector. I have some cases where changes are disabled (DisableGroup) and I would like to also hide the knob while the content can not be changed anyway.
Also to make things easier for users I think about making the field higher (EditorGUIUtility.SingleLineHeight * 2) -> the picker gets stretched as well what looks kind of shitty ^^
example:
using UnityEditor;
using UnityEngine;
public class Bla : MonoBehaviour {
[CustomEditor(typeof(Bla))]
public class BlaEditor : Editor
{
private AudioClip _clip;
public override void OnInspectorGUI()
{
EditorGUI.BeginDisabledGroup(true);
// do some magic to hide the object picker
_clip = (AudioClip) EditorGUILayout.ObjectField("some label", _clip, typeof(AudioClip), false);
EditorGUI.EndDisabledGroup();
}
}
}
I want to stick with an ObjectField rather than a simple Label for two reasons:
Even on a disabled `ObjectField| the "ping" functionality is still working. (If you click on it, the according asset gets highlighted in the Hierarchy.) This is not the case obviously with a label.
The user should not get confused with completely different looking controls but I rather only want to remove some unnecessary clutter.
You might find a solution to hide the object picker by usage of stylesheets.
If all you want is just to display some reference, you can use a simple button basically styled as text field, adding an image and ping the object from code yourself.
using UnityEngine;
namespace Test
{
public class TestBehaviour : MonoBehaviour
{
[SerializeField] private bool _audioEnabled;
[SerializeField] private AudioClip _audioClip;
}
}
editor:
using System.Reflection;
using UnityEditor;
using UnityEditor.Experimental.UIElements;
using UnityEngine;
namespace Test
{
[CustomEditor(typeof(TestBehaviour))]
public class TestBehaviourEditor : Editor
{
private SerializedProperty _clipProp;
private SerializedProperty _audioEnabledProp;
private ObjectField m_ObjectField;
private const BindingFlags FIELD_BINDING_FLAGS = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
private void OnEnable()
{
_clipProp = serializedObject.FindProperty("_audioClip");
_audioEnabledProp = serializedObject.FindProperty("_audioEnabled");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(_audioEnabledProp);
if(_audioEnabledProp.boolValue)
EditorGUILayout.PropertyField(_clipProp);
else
{
//TODO: calculate proper layout
var type = target.GetType().GetField(_clipProp.propertyPath, FIELD_BINDING_FLAGS).FieldType;
var clip = _clipProp.objectReferenceValue;
var guiContent = EditorGUIUtility.ObjectContent(clip, type);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Fake ObjectField Button");
var style = new GUIStyle("TextField");
style.fixedHeight = 16;
style.imagePosition = clip ? ImagePosition.ImageLeft : ImagePosition.TextOnly;
if (GUILayout.Button(guiContent, style ) && clip)
EditorGUIUtility.PingObject(clip);
EditorGUILayout.EndHorizontal();
}
serializedObject.ApplyModifiedProperties();
}
}
}
I've got another solution: Ignore the pick result of this object picker, and although the picker is still here and can show the picker window, pick up will not work.
(I still don't know how to hide this button and the pickerwindow, > <), and this answer was posted at unity answers as well.
Here is the code:
// Register another callback of this object field
myObjectField.RegisterValueChangedCallback(DefaultObjectFieldCallback);
// In this callback, is a trick
private void DefaultAssetFieldCallback(ChangeEvent<UnityEngine.Object> evt) {
// unregister the callback first
myObjectField.UnregisterValueChangedCallback(DefaultAssetFieldCallback);
// trick: set back to the old value
m_ConfigAssetField.value = evt.previousValue;
// register the callback again
myObjectField.RegisterValueChangedCallback(DefaultObjectFieldCallback);
}
I needed to do something similar and found a way to do this by stepping through the ObjectField in the UIToolkit Debugger. The type of the little object selector button is hidden, so we cant really work with the class itself.
This solution is using UIToolkit, so unfortunately it won't work with Unity Editor IMGUI, but hopefully it will be helpful to someone.
The Solution in easy steps:
Find out what the uss style class of the ObjectFieldSelector is.
Recursively search the children of the ObjectField for a VisualElement containing the uss style class.
Set visibility to false.
All done!
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
[CustomPropertyDrawer(typeof(MyClass))]
public class MyClassDrawer: PropertyDrawer
{
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
var field = new ObjectField(property.displayName);
field.objectType = typeof(MyClass);
var button = FindChild(field, "unity-object-field__selector");
button.visible = false;
return field;
}
private VisualElement FindChild(VisualElement parent, string ussClass)
{
foreach(var child in parent.Children())
{
if (child.ClassListContains(ussClass))
return child;
var subChild = FindChild(child, ussClass);
if (subChild != null)
return subChild;
}
return null;
}
}