How to get screen click input from anywhere with c

2019-04-11 19:11发布

TL;DR: How do I implement Unity's 'color from screen' functionality but with vectors?

Image of Unity's color picker


Ok so title is pretty simplified for what I'm trying to do:

  1. Have the user click a button, then click a position on the screen to have that [world] position be saved as the vector. - This is mostly working, except it won't detect left clicks outside of the inspector.

  2. Disable left click for everything else on the unity editor (so when you click a position it doesn't change focus to another GameObject). - This is the main problem.

Tracking the mouse and getting the world position was pretty easy, it's just a bool to save if the mouse is being tracked and a SerializedProperty to save which value the mouse position is being saved to.

Here's what my attribute looks like:

public class VectorPickerAttribute : PropertyAttribute {
    readonly bool relative;

    /// <summary>
    /// Works a lot like the color picker, except for vectors.
    /// </summary>
    /// <param name="relative">Make the final vector relative to the transform?</param>
    public VectorPickerAttribute(bool relative = false) {
        this.relative = relative;
    }
}

Here is the PropertyDrawer:

[CustomPropertyDrawer(typeof(VectorPickerAttribute))]
public class VectorPickerDrawer : PropertyDrawer {
    bool trackMouse = false;
    SerializedProperty v;

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
        if(property.propertyType == SerializedPropertyType.Vector2) {
            Rect button = new Rect(position);
            button.x = position.width - 2;
            button.width = position.height;
            bool pressed = GUI.Button(button, "");
            if(pressed) {
                trackMouse = true;
                v = property;
            }
            else if(Input.GetMouseButtonDown(0)) trackMouse = false;

            bool tracking = trackMouse && v.propertyPath == property.propertyPath;
            if(tracking) {
                property.vector2Value =
                    Camera.main.ScreenToWorldPoint(
                        GUIUtility.GUIToScreenPoint(
                            Event.current.mousePosition
                ));
            }

            GUI.enabled = !tracking;
            EditorGUI.Vector2Field(position, label.text, property.vector2Value);
            GUI.enabled = true;

            EditorUtility.SetDirty(property.serializedObject.targetObject);
        }
    }
}

And here's what it does so far:

Image of VectorPickerAttribute in action.

You click the button on the right, and it will update the vector to the mouse position until it detects a left click with Input.GetMouseButtonDown(0).

Problems with this:

  • It will only detect a click when it's actually on the inspector window.

  • When you click outside the inspector window it will either not change anything or it will select something else so it will close the inspector (but since it saves the mouse position every OnGUI() that point where you clicked will be saved to the vector, so I guess it works??).

I've tried covering the screen with a blank window, but I couldn't get GUI.Window or GUI.ModalWindow to do anything in the PropertyDrawer. I've also tried using GUI.UnfocusWindow(), but either it doesn't work in PropertyDrawer or it's only meant for Unity's windows or something.

标签: unity3d
1条回答
混吃等死
2楼-- · 2019-04-11 19:58

Core aspects:

  • overwrite SceneView.onSceneGUIDelegate in order to catch any mouse events on the SceneView

  • use ActiveEditorTracker.sharedTracker.isLocked to lock and unlock the inspector to prevent losing the focus (which would cause the OnGUI not getting called anymore)

  • use Selection.activeGameObject and set it to the GameObject the drawer is on in order to prevent losing the focus on the GameObject (especially in the moment ActiveEditorTracker.sharedTracker.isLocked is set to false it seems to automatically clear Selection.activeGameObject)

  • Allow reverting the value to previous using the Escape key

  • Use Event.current.Use(); and/or Event.current = null; (I just wanted to be very sure) in order to prevent the event to propagate and being handled by someone else

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// Attribute for setting a vector by clicking on the screen in editor mode.
/// </summary>
public class VectorPickerAttribute : PropertyAttribute {
    public readonly bool relative;

    /// <summary>Works a lot like the color picker, except for vectors.</summary>
    /// <param name="relative">Make the final vector relative the transform?</param>
    public VectorPickerAttribute(bool relative = false) {
        this.relative = relative;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(VectorPickerAttribute))]
public class VectorPickerDrawer : PropertyDrawer {
    #region Variables
    bool _trackMouse;
    SerializedProperty _property;
    MonoBehaviour script;

    ///<summary>Keep the currently selected object to avoid loosing focus while/after tracking</summary>
    GameObject _mySelection;

    ///<summary>For reverting if tracking canceled</summary>
    Vector2 _originalPosition;

    ///<summary>Flag for doing Setup only once</summary>
    bool _setup;

    /// <summary>Mouse position from scene view into the world.</summary>
    Vector2 worldPoint;
    #endregion

    /// <summary>
    /// Catch a click event while over the SceneView
    /// </summary>
    /// <param name="sceneView">The current scene view => might not work anymore with multiple SceneViews</param>
    private void UpdateSceneView(SceneView sceneView) {

        Camera cam = SceneView.lastActiveSceneView.camera;
        worldPoint = Event.current.mousePosition;
        worldPoint.y = Screen.height - worldPoint.y - 36.0f; // ??? Why that offset?!
        worldPoint = cam.ScreenToWorldPoint(worldPoint);

        VectorPickerAttribute vectorPicker = attribute as VectorPickerAttribute;
        if(script != null && vectorPicker.relative) worldPoint -= (Vector2)script.transform.position;

        // get current event
        var e = Event.current;

        // Only check while tracking
        if(_trackMouse) {
            if((e.type == EventType.MouseDown || e.type == EventType.MouseUp) && e.button == 0) {
                OnTrackingEnds(false, e);
            }
            else {
                // Prevent losing focus
                Selection.activeGameObject = _mySelection;
            }
        }
        else {
            // Skip if event is Layout or Repaint
            if(e.type == EventType.Layout || e.type == EventType.Repaint) return;

            // Prevent Propagation
            Event.current.Use();
            Event.current = null;

            // Unlock Inspector
            ActiveEditorTracker.sharedTracker.isLocked = false;

            // Prevent losing focus
            Selection.activeGameObject = _mySelection;

            // Remove SceneView callback
            SceneView.onSceneGUIDelegate -= UpdateSceneView;

        }
    }

    /// <summary>
    /// Called when ending Tracking
    /// </summary>
    /// <param name="revert">flag whether to revert to previous value or not</param>
    /// <param name="e">event that caused the ending</param>
    /// <returns>Returns the vector value of the property that we are modifying.</returns>
    private Vector2 OnTrackingEnds(bool revert, Event e) {
        e.Use();
        Event.current = null;
        //Debug.Log("Vector Picker finished");

        if(revert) {
            // restore previous value
            _property.vector2Value = _originalPosition;
            //Debug.Log("Reverted");
        }

        // disable tracking
        _trackMouse = false;

        // Apply changes
        _property.serializedObject.ApplyModifiedProperties();

        return _property.vector2Value;
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
        script = (MonoBehaviour)property.serializedObject.targetObject;

        if(property.propertyType != SerializedPropertyType.Vector2) {
            EditorGUI.HelpBox(position, "This Attribute requires Vector2", MessageType.Error);
            return;
        }

        var e = Event.current;

        if(!_setup) {
            // store the selected Object (should be the one with this drawer active)
            _mySelection = Selection.activeGameObject;
            _property = property;

            _setup = true;
        }


        // load current value into serialized properties
        _property.serializedObject.Update();

        //specific to the ONE property we are updating
        bool trackingThis = _trackMouse && property.propertyPath == _property.propertyPath;

        GUI.enabled = !trackingThis;
        EditorGUI.PropertyField(position, property, label);
        GUI.enabled = true;


        // Write manually changed values to the serialized fields
        _property.serializedObject.ApplyModifiedProperties();



        if(!trackingThis) {
            var button = new Rect(position) {
                x = position.width - 2,
                width = position.height
            };

            // if button wasn't pressed do nothing
            if(!GUI.Button(button, "")) return;

            // store current value in case of revert
            _originalPosition = _property.vector2Value;

            // enable tracking
            _property = property;
            _trackMouse = true;

            // Lock the inspector so we cannot lose focus
            ActiveEditorTracker.sharedTracker.isLocked = true;

            // Prevent event propagation
            e.Use();

            //Debug.Log("Vector Picker started");
            return;
        }

        // <<< This section is only reached if we are in tracking mode >>>

        // Overwrite the onSceneGUIDelegate with a callback for the SceneView
        SceneView.onSceneGUIDelegate = UpdateSceneView;

        // Set to world position
        _property.vector2Value = worldPoint;

        // Track position until either Mouse button 0 (to confirm) or Escape (to cancel) is clicked
        var mouseUpDown = (e.type == EventType.MouseUp || e.type == EventType.MouseDown) && e.button == 0;
        if(mouseUpDown) {
            // End the tracking, don't revert
            property.vector2Value = OnTrackingEnds(false, e);
        }
        else if(e.type == EventType.KeyUp && _trackMouse && e.keyCode == KeyCode.Escape) {
            // Cancel tracking via Escape => revert value
            property.vector2Value = OnTrackingEnds(true, e);
        }

        property.serializedObject.ApplyModifiedProperties();

        //This fixes "randomly stops updating for no reason".
        EditorUtility.SetDirty(property.serializedObject.targetObject);
    }
}

I tried to explain everything in the comments. Ofcourse this still has some flaws and might not work in some special cases but I hope it gets in the correct direction.

查看更多
登录 后发表回答