IntSlider callback function? Editor Scripting - unity3d

I just got into Editor Scripting recently and I'm still getting a hold of all the basics. I wanted to know if the IntSlider (that we use to create a slider of integer values) has any methods or functions that can be used to get more functionality?
Something like the Slider class:
You have -maxValue, -minValue, -value, -onValueChanged (Callback executed when the value of the slider is changed)
These work during the "Play" mode.
I need to access this information while in the editor. Specifically: I need to access the onValueChanged (if it exists) of the IntSlider so I can assign a function for it to execute.
IntSlider Code:
totalRooms = EditorGUILayout.IntSlider(new GUIContent ("Total Rooms"), totalRooms, 0, 10);
Is there a way to achieve this for the inspector? Or something that can be created to solve this? If you can point me in the right direction, I'd be grateful.
I'm using Unity 5.3.1f

There is no such event in Unity at the moment. However this can be implemented easily like this:
Add a UnityEvent in your actual script.
public UnityEvent OnVariablesValueChanged;
Then in your editor script check if value is changed. (Note: I tried to write easily understandable code sample for this. you can change it accordingly)
[CustomEditor(typeof(MyScript))]
public class MyScriptEditor : Editor
{
public override void OnInspectorGUI()
{
MyScript myTarget = (MyScript)target;
int prevValue = myTarget.variable;
myTarget.variable = EditorGUI.IntSlider(new Rect(0,0,100, 20), prevValue, 1, 10);
int newValue = myTarget.variable;
if (prevValue != newValue)
{
Debug.Log("New value of variable is :" + myTarget.variable);
myTarget.OnVariablesValueChanged.Invoke();
}
base.OnInspectorGUI();
}
}
Then add listener to OnVariablesValueChanged event from inspector.
Hope this helps.

Related

Unity3D New Input System: Is it really so hard to stop UI clickthroughs (or figure out if cursor is over a UI object)?

Even the official documentation has borderline insane recommendations to solve what is probably one of the most common UI/3D interaction issues:
If I click while the cursor is over a UI button, both the button (via the graphics raycaster) and the 3D world (via the physics raycaster) will receive the event.
The official manual:
https://docs.unity3d.com/Packages/com.unity.inputsystem#1.2/manual/UISupport.html#handling-ambiguities-for-pointer-type-input essentially says "how about you design your game so you don't need 3D and UI at the same time?".
I cannot believe this is not a solved problem. But everything I've tried failed. EventSystem.current.currentSelectedGameObject is sticky, not hover. PointerData is protected and thus not accessible (and one guy offered a workaround via deriving your own class from Standalone Input Module to get around that, but that workaround apparently doesn't work anymore). The old IsPointerOverGameObject throws a warning if you query it in the callback and is always true if you query it in Update().
That's all just mental. Please someone tell me there's a simple, obvious solution to this common, trivial problem that I'm just missing. The graphics raycaster certainly stores somewhere if it's over a UI element, right? Please?
I've looked into this a fair bit and in the end, the easiest solution seems to be to do what the manual says and put it in the Update function.
bool pointerOverUI = false;
void Update()
{
pointerOverUI = EventSystem.current.IsPointerOverGameObject();
}
Your frustration is well founded: there are NO examples of making UI work with NewInput which I've found. I can share a more robust version of the Raycaster workaround, from Youtube:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.UI;
/* Danndx 2021 (youtube.com/danndx)
From video: youtu.be/7h1cnGggY2M
thanks - delete me! :) */
public class SCR_UiInteraction : MonoBehaviour
{
public GameObject ui_canvas;
GraphicRaycaster ui_raycaster;
PointerEventData click_data;
List<RaycastResult> click_results;
void Start()
{
ui_raycaster = ui_canvas.GetComponent<GraphicRaycaster>();
click_data = new PointerEventData(EventSystem.current);
click_results = new List<RaycastResult>();
}
void Update()
{
// use isPressed if you wish to ray cast every frame:
//if(Mouse.current.leftButton.isPressed)
// use wasReleasedThisFrame if you wish to ray cast just once per click:
if(Mouse.current.leftButton.wasReleasedThisFrame)
{
GetUiElementsClicked();
}
}
void GetUiElementsClicked()
{
/** Get all the UI elements clicked, using the current mouse position and raycasting. **/
click_data.position = Mouse.current.position.ReadValue();
click_results.Clear();
ui_raycaster.Raycast(click_data, click_results);
foreach(RaycastResult result in click_results)
{
GameObject ui_element = result.gameObject;
Debug.Log(ui_element.name);
}
}
}
So, just drop into my "Menusscript.cs"?
But as a pattern, this is terrible for separating UI concerns. I'm currently rewiring EVERY separately-concerned PointerEventData click I had already working, and my question is, Why? I can't even find how it's supposed to work: to your point there IS no official guide at all around clicking UI, and it does NOT just drop-on-top.
Anyway, I haven't found anything yet which makes new input work easily on UI, and definitely not found how I'm going to sensibly separate Menuclicks from Activityclicks while keeping game & ui assemblies separate.
Good luck to us all.
Unity documentation for this issue with regard to Unity.InputSystem can be found at https://docs.unity3d.com/Packages/com.unity.inputsystem#1.3/manual/UISupport.html#handling-ambiguities-for-pointer-type-input.
IsPointerOverGameObject() can always return true if the extent of your canvas covers the camera's entire field of view.
For clarity, here is the solution which I found worked best (accumulated from several other posts across the web).
Attach this script to your UI Canvas object:
public class CanvasHitDetector : MonoBehaviour {
private GraphicRaycaster _graphicRaycaster;
private void Start()
{
// This instance is needed to compare between UI interactions and
// game interactions with the mouse.
_graphicRaycaster = GetComponent<GraphicRaycaster>();
}
public bool IsPointerOverUI()
{
// Obtain the current mouse position.
var mousePosition = Mouse.current.position.ReadValue();
// Create a pointer event data structure with the current mouse position.
var pointerEventData = new PointerEventData(EventSystem.current);
pointerEventData.position = mousePosition;
// Use the GraphicRaycaster instance to determine how many UI items
// the pointer event hits. If this value is greater-than zero, skip
// further processing.
var results = new List<RaycastResult>();
_graphicRaycaster.Raycast(pointerEventData, results);
return results.Count > 0;
}
}
In class containing the method which is handling the mouse clicks, obtain the reference to the Canvas UI either using GameObject.Find() or a public exposed variable, and call IsPointerOverUI() to filter clicks when over UI.
Reply to #Milad Qasemi's answer
From the docs you have attached in your answer, I have tried the following to check if the user clicked on a UI element or not.
// gets called in the Update method
if(Input.GetMouseButton(0) {
int layerMask = 1 << 5;
// raycast in the UI layer
RaycastHit2D hit = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero, Mathf.Infinity, layerMask);
// if the ray hit any UI element, return
// don't handle player movement
if (hit.collider) { return; }
Debug.Log("Touched not on UI");
playerController.HandlePlayerMovement(x);
}
The raycast doesn't seem to detect collisions on UI elements. Below is a picture of the Graphics Raycaster component of the Canvas:
Reply to #Lowelltech
Your solution worked for me except that instead of Mouse I used Touchscreen
// Obtain the current touch position.
var pointerPosition = Touchscreen.current.position.ReadValue();
An InputSytem is a way to receive new inputs provided by Unity. You can't use existing scripts there, and you'll run into problems like the original questioner. Answers with code like "if(Input.GetMouseButton(0)" are invalid because they use the old system.

Unity editor scripting : how to execute two different actions with the same button?

I wrote some code to add a button to my custom editor in Unity. What I'd like is the following. When I click the button once, a component is added, and when I click again the component is removed. In the simple code below, I just try to print "Add" or "Remove" when clicking the button. I noticed that the variable toggleRigidBody takes the value true but then take the value false right after. It doesn't stay "true" even though I never explicitly change it in the code. The "else if" is never fired. I'm not sure why.
using UnityEditor;
using UnityEngine;
using SS3D.Engine.Inventory;
[CustomEditor(typeof(ContainerController))]
public class ContainerControllerEditor : Editor
{
private bool toggleRigidBody = false;
public override void OnInspectorGUI()
{
DrawDefaultInspector();
ContainerController containerController = (ContainerController)target;
Debug.Log(toggleRigidBody);
bool buttonPressed = GUILayout.Button("AddAttachedContainer");
if (buttonPressed && toggleRigidBody == false)
{
Debug.Log("Add");
toggleRigidBody = true;
}
else if (buttonPressed && toggleRigidBody == true)
{
Debug.Log("Remove");
toggleRigidBody = false;
}
}
}
My code only print "Add" when I click the button. What's happening here ?
The main problem here is that the editor instance is created when the object is clicked and the Inspector loaded. And then it is destroyed as soon as the object loses focus and the Inspector not shown for this object anymore.
=> Your flag toggleRigidBody is not persistent!
What you rather want to do is serialize the flag inside your object or even better: Serialize the reference itself.
This way you
Have already access to the reference in your script in case you need it on runtime
Have the reference in the editor for a) checking if it exists and b) being able to remove it directly
So having your class like
public class ContainerController : MonoBehaviour
{
// Store the reference in a field
[SerializeField] private Rigidbody _rigidbody;
...
}
The editor could look like
[CustomEditor(typeof(ContainerController))]
public class ContainerControllerEditor : Editor
{
private SerializedProperty _rigidbody;
ContainerController containerController;
private void OnEnable()
{
_rigidbody = serializedObject.FindProperty("_rigidbody");
containerController = (ContainerController)target;
}
public override void OnInspectorGUI()
{
DrawDefaultInspector();
// Loads all current serialized values from the target into the serialized properties
serializedObject.Update();
// If the _rigidbody field is not assigned
// try GetComponent as fallback
if(!_rigidbody.objectReferenceValue) _rigidbody.objectReferenceValue = containerController.GetComponent<Rigidbody>();
// simply put everything that belongs to one button click inside one if block
// this is easier to maintain and read
// Of course alternatively you could also simply have two completely different buttons to display
// depending on the value of "_rigidbody.objectReferenceValue"
if(GUILayout.Button(_rigidbody.objectReferenceValue ? "Remove Rigidbody" : "Add Rigidbody")
{
// Is there a Rigidbody?
if(_rigidbody.objectReferenceValue)
{
// Yes -> destroy it
// There are two different destroy methods depending whether you are
// in Play mode or Edit mode
if(Application.isPlaying)
{
Destroy(_rigidbody.objectReferenceValue);
}
else
{
DestroyImmediate(_rigidbody.objectReferenceValue);
}
}
// Otherwise the field is currently not set and no component was found using GetComponent
else
{
// Add the component via the ObjectFactory
// this enabled undo/redo and marks the scene dirty etc
// and assign it to the serialized property
_rigidbody.objectReferenceValue = ObjectFactory.AddComponent<Rigidbody>(target.gameObject);
}
}
// Writes back all modified properties to the target and takes care of Undo/Redo and marking dirty
serializedObject.ApplyModifiedProperties ();
}
}
The editor object is created when it's being displayed, and destroyed when it's not, so for your data to persist you will need to store the values somewhere else. So that's definitely what is happening. The easiest way to have a value of a variable to be persistent between sessions you would use EditorPrefs to save the variable values. Thats the easiest way you can go about it, so you would use this to save the toggleRigidBody value.
https://docs.unity3d.com/ScriptReference/EditorPrefs.SetBool.html

Unity - passing a param along with value in onValueChanged

Ok, so, a Slider in Unity has an "On Value Changed (Single)" property. As far as I've managed so far, you can set on it a callback function that can take a parameter (e.g. a string) which you hardcode in the Inspector, and/or a function that takes a float (the new value that the slider has been set to). However, I'd like to be able to pass both, and I haven't seen a way to do it. For example, I'd like to have a function saveValue(string name, float value), and several sliders feeding their values into it - each slider would have a different name written in the Inspector, and the updated value would be passed as value. I suspect Unity does not support that, but I haven't found any documentation explicitly describing this feature, so I'm not sure. Can this be done?
This should get you started:
Create a class that all of your sliders can access (for example by reference). And put the method you want them to call in there. The method should take the two parameters that you want to use (in my case I want it to take a GameObject and a float):
public class SliderManagerScript : MonoBehaviour
{
public void SliderValueChangeHandler(GameObject slider, float value)
{
Debug.Log("Slider name: " + slider.gameObject.transform.name);
Debug.Log("Slider value: " + value);
}
}
On each of the sliders that you want to call the above method, add a script that can access the above class and add an event listener for the Slider's onValueChanged property. Register a delegate for that listener which calls the method shown above and passes in the appropriate values. E.g
public class SliderScript : MonoBehaviour
{
public GameObject sliderManager;
void Start()
{
GetComponent<Slider>().onValueChanged.AddListener(delegate { sliderManager.GetComponent<SliderManagerScript>().SliderValueChangeHandler(this.gameObject, this.GetComponent<Slider>().value); });
}
}
And that should work fine. In fact, you should be able to simplify it a lot more depending on how you've architected your code.

Unity3D: Custom UnityEvent AddListener not firing

I have my own custom UnityEvent and am trying to add a listener.
I have used AddListener on numerous other UI objects, such as buttons, dropdowns, toggles, etc. so I understand the process. However, when I Invoke my UnityEvent, it simply doesn't fire.
I'm receiving no error messages, and after doing reading and research, everything looks correct. So, not sure what to do further.
This is an object that emits when it's rotated.
This is the basics of my code:
using UnityEngine.Events;
public class Rotator: MonoBehaviour
{
public UnityEvent OnRotate;
int angle = 0;
int newAngle = 0;
void Start()
{
OnRotate = new UnityEvent();
}
void Update()
{
newAngle = (int)transform.rotation.eulersAngles.z;
if (newAngle != angle)
{
print ("Actual Instance ID: " + GetInstanceID());
print ("Invoking!");
OnRotate.Invoke();
angle = newAngle;
}
}
}
and
public class Owner: MonoBehaviour
{
public Rotator rotator;
void Start()
{
print ("Rotator Instance ID: " + rotator.GetInstanceID());
rotator.OnRotate.AddListener(
() => UpdateRotation()
);
}
void UpdateRotation()
{
print ("Invoked!");
}
}
When the Rotator has it's angle changed, I get this in the console:
Actual Instance ID: 11234
Rotator Instance ID: 11234
Invoking!
The instance ID is to make sure I'm working with the same objects and not going in circles for nothing. They match, so I'm listening to the object that's firing.
However, the listener isn't firing. I've tried different combinations with delegates, etc. but it's all the same. No errors. It just doesn't invoke.
Obviously, I'm doing something wrong, but what is it?
Thanks for any help.
Somehow your answered your new edited version of the question with exactly the code you previously provided in the First Version of your Question!
As I tried to tell you ... if you anywhere in your code do OnRotate = new UnityEvent() of course you thereby erase any persistent callbacks and any runtime callbacks added before that moment!
In short
Simply leave it as
public UnityEvent OnRotate;
and you don't even have to think about it anymore.
For understanding why it also works if you put it in Awake please simply have a look at the Order of Execution for Event Functions
&rightarrow; First Awake and OnEnabled is called for every GameObject/Component. Then all Start methods are called as soon as the GameObject/Component is active.
Within each of these blocks (Awake + OnEnable) and (Start) the order of execution between different component types is not guaranteed unless you explicitly configure it via the Script Execution Order Settings where you could define that Owner is simply run before Rotator .. then having both in Start would also work again.
Why does it also work if you do it on the public field?
&rightarrow; Because this field is serialized. That means it is initialized automatically in the Inspector and then stored together with the Scene or prefab asset including any persistent callbacks.
And then Later Unity re-uses the serialized Version of the field so actually you can completely remove the new UnityEvent(); since it doesn't have any effect on a serialized field! It will always be initialized automatically anyway!
Ok, I found out what the issue was.
My question now is "why?".
I changed my code from:
public UnityEvent OnRotate;
void Start() {
OnRotate = new UnityEvent();
}
to
public UnityEvent OnRotate = new UnityEvent();
void Start() {
}
And now it works.
Although, now that I think about it, Awake() is the method where they all fire before initialization, whereas Start() is when the object is created. So the Start() of the Rotator is probably getting called after the Owner is adding a listener.

How I can get uGUI event parameter?

I'm using new event system, represented in unity 4.6.
Here is example on attaching listener to Toggle component.
My question is: "How can I get Boolean parameter from On Value Changed(Boolean)?"
Currently I'm forced to keeping link of that Toggle object in code and checked new value from it.
public void OnVRtoggled() {
var value = toggleVR.isOn;
}
But I think that there must be way of getting boolean value from event. Is that possible?
In Unity 5.1.2 you just need to add a bool parameter in your callback method
public void OnToggledVR(bool isOn)
{
}