I am building a custom window and I am trying to reuse Unity's Scene view to be able to draw directly from this specific window.
I manage to reproduce the correct window by extend UnityEditor.SceneView and here's what I have:
And here's the code:
[EditorWindowTitle(title = "Shape Editor", useTypeNameAsIconName = false)]
public class StrokeEditor : SceneView
{
[MenuItem("Recognizer/Shape Editor")]
public static void Init()
{
var w = GetWindow<StrokeEditor>();
w.in2DMode = true;
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
}
protected override void OnGUI()
{
using (new GUILayout.HorizontalScope())
{
GUILayout.Button("Add Stroke");
GUILayout.Button("Edit Stroke");
GUILayout.Button("Delete Stroke");
}
base.OnGUI();
}
}
With this, I might be almost done.
Is this the right way to procede ?
I feel that something is wrong because whenever I use EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);, it creates also a new scene to the main scene view. (I want the main scene view to stay unchanged)
I should also be able to see the tools from the scene view like:
Is there any better way to achieve what I want ?
EDIT 1:
The final usage of all of this is to be able to draw 2D shapes within the window by clicking and dragging the mouse like gestures with mobile phones. From that, I'll be able to get some of the position to feed one of my algorithm...
You can use the new GraphView. This gives you some of the things you are looking for for "free", mainly to zoom and pan the view. Since ShaderGraph uses this, it should be easier to construct nodes, select them and move them around, if that is something you want to.
Here is a toy example of a custom editor window that allows you to edit list of points in a scriptable object:
Shape.cs - simple scriptable object with a list of points.
[CreateAssetMenu(menuName = "Test/ShapeObject")]
public class Shape : ScriptableObject
{
public List<Vector2> PointList = new List<Vector2>();
}
ShapeEditorWindow.cs - editor window with a toolbar and a graphview that opens scriptable objects of type Shape.
using UnityEngine;
using UnityEditor;
using UnityEditor.UIElements;
public class ShapeEditorWindow : EditorWindow
{
private ShapeEditorGraphView _shapeEditorGraphView;
private Shape _shape;
[UnityEditor.Callbacks.OnOpenAsset(1)]
private static bool Callback(int instanceID, int line)
{
var shape = EditorUtility.InstanceIDToObject(instanceID) as Shape;
if (shape != null)
{
OpenWindow(shape);
return true;
}
return false; // we did not handle the open
}
private static void OpenWindow(Shape shape)
{
var window = GetWindow<ShapeEditorWindow>();
window.titleContent = new GUIContent("Shape Editor");
window._shape = shape;
window.rootVisualElement.Clear();
window.CreateGraphView();
window.CreateToolbar();
}
private void CreateToolbar()
{
var toolbar = new Toolbar();
var clearBtn = new ToolbarButton(()=>_shape.PointList.Clear()); ;
clearBtn.text = "Clear";
var undoBtn = new ToolbarButton(() =>_shape.PointList.RemoveAt(_shape.PointList.Count-1));
undoBtn.text = "Undo";
toolbar.Add(clearBtn);
toolbar.Add(new ToolbarSpacer());
toolbar.Add(undoBtn);
rootVisualElement.Add(toolbar);
}
private void CreateGraphView()
{
_shapeEditorGraphView = new ShapeEditorGraphView(_shape);
_shapeEditorGraphView.name = "Shape Editor Graph";
rootVisualElement.Add(_shapeEditorGraphView);
}
}
ShapeEditorGraphView.cs - graphview with zoom, grid, pan (with ContentDragger) and shape editor.
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
public class ShapeEditorGraphView : GraphView
{
const float _pixelsPerUnit = 100f;
const bool _invertYPosition = true;
public ShapeEditorGraphView(Shape shape){
styleSheets.Add(Resources.Load<StyleSheet>("ShapeEditorGraph"));
this.StretchToParentSize();
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
Add(new GridBackground());
//pan with Alt-LeftMouseButton drag/ MidleMouseButton drag
this.AddManipulator(new ContentDragger());
//other things that might interest you
//this.AddManipulator(new SelectionDragger());
//this.AddManipulator(new RectangleSelector());
//this.AddManipulator(new ClickSelector());
this.AddManipulator(new ShapeManipulator(shape));
contentViewContainer.BringToFront();
contentViewContainer.Add(new Label { name = "origin", text = "(0,0)" });
//set the origin to the center of the window
this.schedule.Execute(() =>
{
contentViewContainer.transform.position = parent.worldBound.size / 2f;
});
}
public Vector2 WorldtoScreenSpace(Vector2 pos)
{
var position = pos * _pixelsPerUnit - contentViewContainer.layout.position;
if (_invertYPosition) position.y = -position.y;
return contentViewContainer.transform.matrix.MultiplyPoint3x4(position);
}
public Vector2 ScreenToWorldSpace(Vector2 pos)
{
Vector2 position = contentViewContainer.transform.matrix.inverse.MultiplyPoint3x4(pos);
if (_invertYPosition) position.y = -position.y;
return (position + contentViewContainer.layout.position) / _pixelsPerUnit;
}
}
Unfortunately the grid background and the grid lines are the same color, so in order to see the grid lines we have to write a style sheet and set the GridBackground properties. This file has to be in Editor/Resources, and gets loaded with styleSheets.Add(Resources.Load<StyleSheet>("ShapeEditorGraph"));
Editor/Resources/ShapeEditorGraph.uss
GridBackground {
--grid-background-color: rgba(32,32,32,1);
--line-color: rgba(255,255,255,.1);
--thick-line-color: rgba(255,255,255,.3);
--spacing: 100;
}
ShapeManipulator.cs - draws and edits the shape. This is similar to RectangleSelector.
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UIElements;
public class ShapeManipulator : MouseManipulator
{
private Shape _shape;
private ShapeDraw _shapeDraw;
public ShapeManipulator(Shape shape)
{
activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse });
_shape = shape;
_shapeDraw = new ShapeDraw { points = shape.PointList };
}
protected override void RegisterCallbacksOnTarget()
{
target.Add(_shapeDraw);
target.Add(new Label { name = "mousePosition", text = "(0,0)" });
target.RegisterCallback<MouseDownEvent>(MouseDown);
target.RegisterCallback<MouseMoveEvent>(MouseMove);
target.RegisterCallback<MouseCaptureOutEvent>(MouseOut);
target.RegisterCallback<MouseUpEvent>(MouseUp);
}
protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<MouseDownEvent>(MouseDown);
target.UnregisterCallback<MouseUpEvent>(MouseUp);
target.UnregisterCallback<MouseMoveEvent>(MouseMove);
target.UnregisterCallback<MouseCaptureOutEvent>(MouseOut);
}
private void MouseOut(MouseCaptureOutEvent evt) => _shapeDraw.drawSegment = false;
private void MouseMove(MouseMoveEvent evt)
{
var t = target as ShapeEditorGraphView;
var mouseLabel = target.Q("mousePosition") as Label;
mouseLabel.transform.position = evt.localMousePosition + Vector2.up * 20;
mouseLabel.text = t.ScreenToWorldSpace(evt.localMousePosition).ToString();
//if left mouse is pressed
if ((evt.pressedButtons & 1) != 1) return;
_shapeDraw.end = t.ScreenToWorldSpace(evt.localMousePosition);
_shapeDraw.MarkDirtyRepaint();
}
private void MouseUp(MouseUpEvent evt)
{
if (!CanStopManipulation(evt)) return;
target.ReleaseMouse();
if (!_shapeDraw.drawSegment) return;
if (_shape.PointList.Count == 0) _shape.PointList.Add(_shapeDraw.start);
var t = target as ShapeEditorGraphView;
_shape.PointList.Add(t.ScreenToWorldSpace(evt.localMousePosition));
_shapeDraw.drawSegment = false;
_shapeDraw.MarkDirtyRepaint();
}
private void MouseDown(MouseDownEvent evt)
{
if (!CanStartManipulation(evt)) return;
target.CaptureMouse();
_shapeDraw.drawSegment = true;
var t = target as ShapeEditorGraphView;
if (_shape.PointList.Count != 0) _shapeDraw.start = _shape.PointList.Last();
else _shapeDraw.start = t.ScreenToWorldSpace(evt.localMousePosition);
_shapeDraw.end = t.ScreenToWorldSpace(evt.localMousePosition);
_shapeDraw.MarkDirtyRepaint();
}
private class ShapeDraw : ImmediateModeElement
{
public List<Vector2> points { get; set; } = new List<Vector2>();
public Vector2 start { get; set; }
public Vector2 end { get; set; }
public bool drawSegment { get; set; }
protected override void ImmediateRepaint()
{
var lineColor = new Color(1.0f, 0.6f, 0.0f, 1.0f);
var t = parent as ShapeEditorGraphView;
//Draw shape
for (int i = 0; i < points.Count - 1; i++)
{
var p1 = t.WorldtoScreenSpace(points[i]);
var p2 = t.WorldtoScreenSpace(points[i + 1]);
GL.Begin(GL.LINES);
GL.Color(lineColor);
GL.Vertex(p1);
GL.Vertex(p2);
GL.End();
}
if (!drawSegment) return;
//Draw current segment
GL.Begin(GL.LINES);
GL.Color(lineColor);
GL.Vertex(t.WorldtoScreenSpace(start));
GL.Vertex(t.WorldtoScreenSpace(end));
GL.End();
}
}
}
The is just example code. The goal was to have something working and drawing to the screen.
I have tackled something similar to this in the past. When I wanted to extend the SceneView, I've used Gizmos and drawing callbacks to add my own controls to the scene view, but I suspect you might want more freedom than just this.
The other thing I did was to create an "editor preview scene", add a camera to it and made the camera render into my custom EditorWindow. It's a lot of work, but once I did it I was completely free to customize the editor experience.
It's probably quite dangerous inheriting from Unity's SceneView, as I expect that it will change so often that you may struggle to get your stuff working on multiple versions. You might also find yourself breaking stuff when Unity's code doesn't expect anyone to be inheriting from SceneView.
Related
I'm trying to apply a simple, rectangular image over faces that are detected in a webcam utilizing of OpenCVSharp. I did manage to make the detecting part by applying a rectangle around the faces, but I'm not sure how to be able to apply an image of the size (and position) of the region of interest.
I created a Mat for the image, but when I copy that image to the display frame, it takes over the entire dimension of the frame instead of only the face region.
How can I apply the desired effect of an image over the marked face region? Do I need to blend two Mats togheter? If so, how?
Code for the face detection
using UnityEngine;
using OpenCvSharp;
public class FaceDetection : MonoBehaviour
{
CascadeClassifier cascade;
public bool initialized = false;
public int totalFaces = 0;
public Mat maskImage;
private void OnEnable()
{
WebcamManager.onFrameProcessed += ProcessFrame;
}
private void OnDisable()
{
WebcamManager.onFrameProcessed -= ProcessFrame;
}
public void Start()
{
var dataPath = Application.dataPath;
//For some reason, Cv2.ImRead always returned an empty Mat, so I created my own class for the conversion, it will be below this code.
maskImage = MyConverter.ConvertToMat(Application.dataPath + #"/Sprites/facemask.png"); //This image is 420x420
Debug.Log(maskImage); //this returns: Mat [ 420*420*CV_8UC1, IsContinuous=True, IsSubmatrix=False, Ptr=0x23eae88bf50, Data=0x23ee94f0040 ]
cascade = new CascadeClassifier(Application.dataPath + #"/haarcascade_frontalface_default.xml");
initialized = true;
}
//This is called in a delegate of a Webcam script, all it does is update the webcam frame and then call this method.
public void ProcessFrame(WebCamTexture texture)
{
if (texture == null || !initialized) return;
Mat frame = OpenCvSharp.Unity.TextureToMat(texture);
var faces = cascade.DetectMultiScale(frame,1.2,1,HaarDetectionType.ScaleImage);
if (faces.Length >= 1)
{
for (int i = 0; i < faces.Length; i++)
{
DisplayFace(frame, faces[i]);
}
}
totalFaces = faces.Length;
}
public void DisplayFace(Mat frame, OpenCvSharp.Rect face)
{
Debug.Log("Face width and height: " + face.Height.ToString() + " x " + face.Width.ToString());
//Here I try to resize the image that is to be applied over the faces, but it ends up taking the whole frame...
Cv2.Resize(maskImage, maskImage, new OpenCvSharp.Size(face.Height, face.Width));
maskImage.CopyTo(frame);
//Draws a blue square around all detected faces.
frame.Rectangle(face, new Scalar(250,0,0), 2);
Texture newTexture = OpenCvSharp.Unity.MatToTexture(frame);
WebcamManager.instance.rend.GetComponent<Renderer>().material.mainTexture = newTexture;
}
}
Converter Class
using UnityEngine;
using OpenCvSharp;
using System.IO;
public static class MyConverter
{
//https://stackoverflow.com/questions/57497491/opencvsharp-imread-function-cant-load-image-in-unity-on-macos
public static Mat ConvertToMat(string filePath)
{
Mat matResult = null;
byte[] fileData = File.ReadAllBytes(filePath);
var tex = new Texture2D(420,420);
tex.LoadImage(fileData);
matResult = OpenCvSharp.Unity.TextureToMat(tex);
Cv2.CvtColor(matResult, matResult, ColorConversionCodes.BGR2GRAY);
return matResult;
}
}
Code for the Webcam Manager
using UnityEngine;
public class WebcamManager : MonoBehaviour
{
public static WebcamManager instance;
public delegate void ProcessFrame(WebCamTexture _wbt);
public static event ProcessFrame onFrameProcessed;
[Header("Settings")]
public int startingIndex = 0; //The index of the camera that will be started by the manager.
[Header("Components")]
public Renderer rend;
private WebCamTexture wbt;
[Header("Status")]
public bool isPlaying = false;
private void Awake()
{
if (!instance) instance = this;
}
private void Start()
{
StartWebcam(0);
}
public void StartWebcam(int index)
{
WebCamDevice[] devices = WebCamTexture.devices;
if (devices.Length <= 0)
{
Debug.LogError("[WebcamManager] There are no camera devices attached to the system!");
return;
}
if (index < devices.Length)
{
Debug.Log("[WebcamManager] Playing camera device: " + devices[index].name);
wbt = new WebCamTexture(devices[index].name);
wbt.Play();
isPlaying = true;
}
}
public void StopWebcam()
{
if (wbt) wbt.Stop();
isPlaying = false;
}
public void UpdateWebcamTexture()
{
if (!isPlaying) return;
if (rend)
{
rend.material.mainTexture = wbt;
}
if (onFrameProcessed != null && wbt) onFrameProcessed(wbt);
}
private void Update()
{
UpdateWebcamTexture();
}
}
I'm new to Unity. How to make Canvas UI Responsive like Small Screens for Small Button and Big Screens for Big button. But when I tested Small buttons in Small Screen are big and Big buttons are Big screens are small. I just wanna reverse this so it will be responsive, Is it possible to do it?
The trick to fully scalable designs in Unity UI, is to never use pixel sizes for anything. Start will all rects fully expanded (0 margins on all sieds) and work your way using anchor points (use alt to force margin zeroing)
I can also recommend a little utility I wrote RectAnchorHelper.
If you follow the link,
You may get an error with [exposemethodineditor] dependency, just comment it out, or download the whole repo. I am pasting full class code with dependency stripped below.
What RectAnchorHelper does it exposes four sliders that let you adjust your placement in fully proportional way, so you can design your UI without any reference to pixel sizes.
The other part of the equation is using the canvas scaler set to Scale with Screen Size as mentioned by Rakib
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
#if UNITY_EDITOR
using UnityEditor;
#endif
[ExecuteInEditMode]
public class RectAnchorHelper : MonoBehaviour
{
RectTransform rect { get { if (_rect == null) _rect = GetComponent<RectTransform>(); return _rect; } }
RectTransform _rect;
[SerializeField] bool edit;
[SerializeField] bool _symmetricalXMode;
[SerializeField] bool _symmetricalYMode;
public bool symmetricalXMode { get { return _symmetricalXMode; } set { _symmetricalXMode = value; CheckAndSet(); } }
public bool symmetricalYMode { get { return _symmetricalYMode; } set { _symmetricalYMode = value; CheckAndSet(); } }
[Range(0, 1)]
[SerializeField] float _xAnchorMin;
[Range(0, 1)]
[SerializeField] float _xAnchorMax = 1;
[Range(0, 1)]
[SerializeField] float _yAnchorMin;
[Range(0, 1)]
[SerializeField] float _yAnchorMax = 1;
public float xAnchorMin { get { return _xAnchorMin; } set { _xAnchorMin = value; CheckAndSet(); } }
public float xAnchorMax { get { return _xAnchorMax; } set { _xAnchorMax = value; CheckAndSet(); } }
public float yAnchorMin { get { return _yAnchorMin; } set { _yAnchorMin = value; CheckAndSet(); } }
public float yAnchorMax { get { return _yAnchorMax; } set { _yAnchorMax = value; CheckAndSet(); } }
// [SerializeField] [HideInInspector] Vector2 offsetMin;
// [SerializeField] [HideInInspector] Vector2 offsetMax;
public void SetMargin(float f) { margin = f; }
[Range(-1, 15)]
[SerializeField] float _margin = -1;
public float margin { get { return _margin; } set { _margin = value; CheckAndSet(); } }
void CheckAndSet()
{
if (symmetricalXMode) _xAnchorMax = 1 - _xAnchorMin;
if (symmetricalYMode) _yAnchorMax = 1 - _yAnchorMin;
SetValues();
}
void Reset()
{
PrepareRect();
GetValues();
}
void OnValidate()
{
if (symmetricalXMode) xAnchorMax = 1 - xAnchorMin;
if (symmetricalYMode) yAnchorMax = 1 - yAnchorMin;
// if (Application.isPlaying) return;
#if UNITY_EDITOR
bool isSelected = false;
foreach (var s in Selection.gameObjects)
{
if (s == gameObject) isSelected = true;
}
if (!isSelected) { edit = false; return; }
Undo.RecordObject(rect, "RectAnchor");
#endif
if (edit)
{
SetValues();
}
else
GetValues();
}
void SetValues()
{
rect.anchorMin = new Vector2(xAnchorMin, yAnchorMin);
rect.anchorMax = new Vector2(xAnchorMax, yAnchorMax);
if (margin != -1)
{
rect.offsetMin = new Vector2(margin * margin, margin * margin);
rect.offsetMax = new Vector2(-(margin * margin), -(margin * margin));
}
}
void GetValues()
{
_xAnchorMin = rect.anchorMin.x;
_xAnchorMax = rect.anchorMax.x;
_yAnchorMin = rect.anchorMin.y;
_yAnchorMax = rect.anchorMax.y;
}
void PrepareRect()
{
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.offsetMax = Vector2.zero;
rect.offsetMin = Vector2.zero;
rect.localScale = Vector3.one;
}
}
Of course you can do it all without my class, but I personally found typing in float values rather tedious, and the above code will give you sliders enabling easy visual editing.
In the Canvas, you have a CanvasScaler
Set UI Scale Mode -> Scale with Screen Size
Enter a base resolution (eg: 1080x1920 works good for mobile) and make sure your game window is on that resolution
Set Match Accordingly (eg: For portrait resolution I use 1)
public class ABCD : MonoBehaviour
{
[SerializeField]
Outline outline;
[SerializeField]
Shadow shadow;
private void Start()
{
outline = GetComponent<Outline>();
shadow = GetComponent<Shadow>();
}
}
we knew Outline is Implementation Shadow like this.
So When I call
shadow = GetCompoent<Shadow>();
It Possible find out the Outline Compoent , because Ouline is also a Shadow.
My question is How can I get the right Compoent?
And I can't drop the compoent to keep reference.
Because My code exactly like this
//this not a monobehavior
class MyText
{
Shadow shadow;
Outline outline;
public MyText(Transfrom transfrom)
{
outline = transfrom.GetComponent<Outline>();
shadow = transfrom.GetComponent<Shadow>();
}
}
If I create Compoent to keep reference, it will use more cost.
Use GetCompoents Can slove that , Anyone have better solution?
foreach (Shadow s in GetComponents<Shadow>())
{
if (s is Outline)
{
outline = s as Outline;
}
else
{
shadow = s;
}
}
The GetComponent will always return the first available member of asked type.
You need to use Get Components to get more than one. Here's an example from https://unitygem.wordpress.com/getcomponent-in-c/
using UnityEngine;
using System.Collections;
public class Health : MonoBehaviour
{
private Force [] scripts = null;
[SerializeField] private int health = 5;
private void Start()
{
this.scripts = this.gameObject.GetComponents<Force>();
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
for (int i = 0; i < this.scripts.Length; i++)
{
if(this.scripts[i].ReportForce())
{
this.health += 5;
}
}
}
}
}
I could do with some help. I have a radar which displays objects. I'm trying to introduce a radar scan feature so that when a button is clicked the image on the radar is updated based on the tag of the object. My code has no errors but I can't get it to work and was hoping someone here could spot what the problem is. Thanks!!!!
RadarScan Script
public class RadarScan : MonoBehaviour {
public Image RadarImageToChange;
public void ChangeImage(Image UpdateImage)
{
if(gameObject.tag == "Enemy")
{
UpdateImage = RadarImageToChange;
}
}
Radar Script
public class RadarObject
{
public Image icon { get; set; }
public GameObject owner { get; set; }
}
public class Radar : MonoBehaviour {
public Transform playerPos; //position of player
float mapScale = 0.1f; //scale radar size
public static List<RadarObject> radObjects = new List<RadarObject>();
//Registers Object to the radar
public static void RegisterRadarObject(GameObject o, Image i)
{
Image image = Instantiate(i);
radObjects.Add(new RadarObject() { owner = o, icon = image }); //adds to List
}
//It loops through the list looking for the owner existing in the list, when it finds the owner is detroys the icon
public static void RemoveRadarObject(GameObject o)
{
//New list for destroyed objects
List<RadarObject> newList = new List<RadarObject>();
for (int i = 0; i < radObjects.Count; i++)
{
if (radObjects[i].owner == o)
{
Destroy(radObjects[i].icon);
continue;
}
else
newList.Add(radObjects[i]);
}
radObjects.RemoveRange(0, radObjects.Count);
radObjects.AddRange(newList);
}
void DrawRadarDots()
{
//loops through the list and for each Object it gets the owners transform position and determins the difference between it's
//position and the players position, does calculations on the angle and distance and position on a circle using polar equations.
foreach (RadarObject ro in radObjects)
{
Vector3 radarPos = (ro.owner.transform.position - playerPos.position);
float distToObject = Vector3.Distance(playerPos.position, ro.owner.transform.position) * mapScale;
float deltay = Mathf.Atan2(radarPos.x, radarPos.z) * Mathf.Rad2Deg - 270 - playerPos.eulerAngles.y;
radarPos.x = distToObject * Mathf.Cos(deltay * Mathf.Deg2Rad) * -1;
radarPos.z = distToObject * Mathf.Sin(deltay * Mathf.Deg2Rad);
//grabs icon of players objects and make it a child of panel and set it's postion based on radarPos.x and radarPos.z
ro.icon.transform.SetParent(this.transform);
ro.icon.transform.position = new Vector3(radarPos.x, radarPos.z, 0) + this.transform.position;
}
}
//Update is called once per frame
void Update ()
{
DrawRadarDots();
}
}
MakeRadarObject Script
public class MakeRadarObject : MonoBehaviour {
public Image image;
// Use this for initialization
void Start () {
Radar.RegisterRadarObject(this.gameObject, image);
}
void OnDestroy()
{
Radar.RemoveRadarObject(this.gameObject);
}
}
You aren't applying the Image to your gameobject, only to a variable named UpdateImage. You need to get the image component of your gameobject and then assign the new image to it. You will also need to change the Image to the form of a Sprite for this to work.
public Sprite RadarImageToChange;
public void ChangeImage(Sprite UpdateImage)
{
if(gameObject.tag == "Enemy")
{
gameObject.GetComponent<Image>().sprite = RadarImageToChange;
}
}
Okay So I'm trying to create an interactive pause menu that has access to Input.GetAxis using Unity 4.6's new UI.
As long as the Navigation relationships are set up for the selectable elements (In my case a series of buttons) and Interactable is set to true then the UI works just fine but, when I set the Time.timeScale = 0.0f; then keyboard input navigation no longer works. However the mouse input clicks and animation still work as intended.
I've tried several different ways to get around the time scale problem and the best I've got so far is checking the value returned from Input.GetAxis() while in the body of the Update message of the MonoBehavor base object. This somewhat works but my results are either the very top or very bottom of the Button selected collection. I'm thinking this is because update gets called a great deal more than FixedUpdate and would make sense if my console printed out more call to the method that moves up and down the selection. So with that I'm thinking its one of those "office space" type errors, off by 1 decimal place or something silly but I just can't seem to see it. Otherwise I think my solution would be a fairly viable work around.
The following is an image of my Unity Setup with mentioned game objects in Unity 4.6 followed by my code.
public class PlayerInGameMenu : MonoBehaviour
{
public EventSystem eventSystem;
Selectable SelectedButton;
public Selectable Status;
public Selectable Settings;
public Selectable Save;
public Selectable Quit;
public bool Paused;
List<Selectable> buttons;
int selecteButtonIndex = 0;
public Canvas Menu;
void Start()
{
Menu.enabled = false;
buttons = new List<Selectable>();
buttons.Add(Status);
buttons.Add(Settings);
buttons.Add(Save);
buttons.Add(Quit);
SelectedButton = buttons[0];
}
void Update()
{
CheckInput();
if (Paused && !Menu.enabled)
{
ShowMenu();
}
else if (!Paused && Menu.enabled)
{
HideMenu();
}
}
void ShowMenu()
{
Paused = true;
Menu.enabled = true;
Time.timeScale = 0.0f;
}
void HideMenu()
{
if (Menu.enabled)
{
Paused = false;
Menu.enabled = false;
Time.timeScale = 1.0f;
}
}
void CheckInput()
{
if (cInput.GetKeyDown("Pause"))
{
Paused = !Paused;
SelectedButton = buttons[selecteButtonIndex];
eventSystem.SetSelectedGameObject(SelectedButton.gameObject, new BaseEventData(eventSystem));
}
if (Paused)
{
float v = cInput.GetAxis("Vertical");
//to attempt to cut down on the input sensitity I am checking 0.5 instead of just 0.0
if (v >= 0.5)
{
GoDown();
}
else if (v <= -0.5)
{
GoUp();
}
}
}
void GoDown()
{
//go to the last button available if we go past the index
if (selecteButtonIndex > buttons.Count - 1)
{
selecteButtonIndex = buttons.Count - 1;
}
else
{
selecteButtonIndex = selecteButtonIndex + 1;
}
}
//go to the first button available if we go past the index
void GoUp()
{
if (selecteButtonIndex < 0)
{
selecteButtonIndex = 0;
}
else
{
selecteButtonIndex = selecteButtonIndex - 1;
}
}
}
I know its in beta but I'm wondering if you are going to implement navigation why would you design it in such a way that Time.timeScale=0.0f; (the easy way to pause a game) does not work with the UI button navigation naturally. Problems for minds greater than I maybe? Or there is a simple way to do it and I just do not know what bit I need to flip.
I've also considered just freezing rigid bodies on pause but that seems like will require a huge time investment in my existing code base and will not be a universal solution across all game objects particularly colliders that do not rely on Rigid bodies and particle systems. I'm pretty open minded about solutions but it seems like there should be a really easy way to do this.
This worked like a charm:
var v = Input.GetAxisRaw("JoyY1"); // Or "Vertical"
if (Math.Abs(v) > ButtonThreashold)
{
var currentlySelected = EventSystem.currentSelectedGameObject
? EventSystem.currentSelectedGameObject.GetComponent<Selectable>()
: FindObjectOfType<Selectable>();
Selectable nextToSelect = null;
if (v > ButtonThreashold)
{
nextToSelect = currentlySelected.FindSelectableOnUp();
}
else if (v < -ButtonThreashold)
{
nextToSelect = currentlySelected.FindSelectableOnDown();
}
if (nextToSelect)
{
EventSystem.SetSelectedGameObject(nextToSelect.gameObject);
}
}
Okay so my solution to this problem was to utilize Time.realtimeSinceStartup to check for input on fixed intervals and develop an abstract class that inherits from MonoBehavior. What that looks like in code:
public abstract class RealtimeMonoBehavior:MonoBehaviour
{
public float updateInterval = 0.5F;
private double lastInterval;
void Start()
{
DefaultIntervalStart();
lastInterval = Time.realtimeSinceStartup;
RealtimeIntervalStart();
}
void Update()
{
DefaultIntervalUpdate();
float timeNow = Time.realtimeSinceStartup;
if (timeNow > lastInterval + updateInterval)
{
lastInterval = timeNow;
RealtimeIntervalUpdate();
}
}
public virtual void DefaultIntervalUpdate(){}
public virtual void DefaultIntervalStart(){}
public virtual void RealtimeIntervalStart(){}
public virtual void RealtimeIntervalUpdate(){}
}
And here is what my code looks like after implementing the change
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using Extensions;
using UnityEngine.EventSystems;
public class PlayerInGameMenu : RealtimeMonoBehavior
{
public EventSystem eventSystem;
Selectable SelectedButton;
public Selectable Status;
public Selectable Settings;
public Selectable Save;
public Selectable Quit;
public float ButtonThreashold;
public bool Paused;
List<Selectable> buttons;
int selectedButtonIndex;
public PlayerMovement PlayerMovement;
public Canvas Menu;
public override void RealtimeIntervalStart()
{
base.RealtimeIntervalStart();
Menu.enabled = false;
buttons = new List<Selectable>();
buttons.Add(Status);
buttons.Add(Settings);
buttons.Add(Save);
buttons.Add(Quit);
selectedButtonIndex = 0;
SelectedButton = buttons[selectedButtonIndex];
eventSystem.SetSelectedGameObject(SelectedButton.gameObject, new BaseEventData(eventSystem));
}
public override void DefaultIntervalUpdate()
{
base.DefaultIntervalUpdate();
if (cInput.GetKeyDown("Pause"))
{
Paused = !Paused;
}
}
public override void RealtimeIntervalUpdate()
{
base.RealtimeIntervalUpdate();
CheckInput();
if (Paused && !Menu.enabled)
{
ShowMenu();
}
else if (!Paused && Menu.enabled)
{
HideMenu();
}
}
void ShowMenu()
{
Paused = true;
Menu.enabled = true;
Time.timeScale = 0.0f;
}
void HideMenu()
{
if (Menu.enabled)
{
Paused = false;
Menu.enabled = false;
Time.timeScale = 1.0f;
}
}
void CheckInput()
{
if (Paused)
{
float v = Input.GetAxisRaw("Vertical");
if (v > ButtonThreashold)
{
GoUp();
}
else if (v < -ButtonThreashold)
{
GoDown();
}
SelectedButton = buttons[selectedButtonIndex];
eventSystem.SetSelectedGameObject(SelectedButton.gameObject, new BaseEventData(eventSystem));
}
}
void GoDown()
{
if (selectedButtonIndex < buttons.Count - 1)
{
selectedButtonIndex = selectedButtonIndex + 1;
}
}
void GoUp()
{
if (selectedButtonIndex > 0)
{
selectedButtonIndex = selectedButtonIndex - 1;
}
}
}
Nod to imapler, Input.GetAxisRaw feels better for checking the input.