Closed maxouellet closed 7 years ago
LIFO?
Last In, First Out. It's a stack
When will this be ready for testing?
Good question. I'm aiming for the week of the 24th. Current plan is to initially put this in a separate branch, give it a few weeks to communicate the breaking changes + stabilize as needed, and then RI into master.
I have this working locally on a few projects, but there's some cleanup I need to do beforehand to cleanly integrate it. Notably, this comes with some other utility fixes and additions that might have ripple effects, such as a modified Singleton that doesn't call FindByType.
@maxouellet Any News?
Have you considered using Reactive Extensions (UniRx)? It allows compact and robust code specially when handling asynchronous inputs from multiple sources. Here is a reactive version of the GazeGestureManager for the Origami sample: https://github.com/aalmada/OrigamiRx/blob/master/Assets/Scripts/GazeGestureManager.cs
I was considering UniRx as well, but I didn't like that it was essentially using Linq.
Were you able to record definitive performance tests?
The Linq performance issue was already raised in the Hololens forum and here is the clarification by the UniRx developer: https://forums.hololens.com/discussion/comment/7938/#Comment_7938
@HodgsonSDAS I'll likely get started on this today or tomorrow. I have all the code written for a first version, it's just a matter of cleaning it up so that i can publish it externally. My schedule is clearing up this afternoon, so hoping to have a first pull request out by end of the week,
@aalmada I have not considered UnirRx. My understanding is that we want to keep HoloToolkit as light and standalone as possible, and also make it simple to use for new developers. While I have no doubt that UniRx is great, I'm not convinced that we should add it as a mandatory dependency to the core HoloToolkit. That being said, this can definitely be brought up in a separate discussion if someone is willing to do the work. Maybe it could be an optional extension to HoloToolkit, or something like that.
I agree with Max. We should keep HoloToolkit strongly typed, easy to read code and extensible. Personally, I felt UnirRx did not meet the readable code criteria. I don't doubt its power but I'd rather not take a dependency on that less known design pattern. Interface/Singleton based patterns are understood by all. If not then they can be easy to ramp up to. Hope this helps!
@maxouellet are you planning on switching back to the singleton that requires setting the instance in awake?
Max here are some questions/thoughts based on the interface diagram:
Gaze: Since we have named all classes so far related to Gaze to be Gaze*something does it makes sense to call IFocusable to be IGazeable and OnGazeEnter and OnGazeExit? This might have the transition easier as well for current code bases.
Gesture:
Voice:
I totally agree with @maxouellet reasoning but not with @NeerajW . Rx is strongly typed, easier to read and easier to extend. For these same reasons so many people are now adopting functional languages. I already started working on a reactive version of HoloToolkit but I'm still trying to figure out the best way to take advantage of your work without replicating it. If it was natively reactive it would be much simpler and that's why I raised the issue. Thanks a lot for the feedback and for all the amazing work.
@darax Yes, but we have improvements to it so that it automatically sets itself up in Awake. There are two drawbacks to this, but at least it's better coding practice than doing FindByType across your whole scene.
var is the opposite of strongly typed. :)
Having a function like .RefCount is a hint that people working on it will need to understand object lifetime of Rx. This makes it harder to extend without introducing leaks or crashes.
I don't consider Linq to be more readable. The script you posted is hard for me to follow. What does .Publish() do? How does the focused object get set? I'd expect to see a raycast somewhere to set the focused object, but I don't see that.
I like where it looks like Max is going with the interfaces, and I'm sure that Rx can be wrapped around that once he's done.
FindByType is only called once per singleton. I'm not sure why that's a problem.
I agree, Linq isn't explicitly typed and can be confusing to read at first, but the definitive answer lies in the hard data of performance testing.
@maxouellet how different is this new singleton generic? What was wrong with the current implementation?
I'll make it simpler by just sharing out what we're using right now.
/// <summary>
/// Singleton behaviour class, used for components that should only have one instance
/// </summary>
/// <typeparam name="T"></typeparam>
public class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
private static T instance;
public static T Instance
{
get
{
return instance;
}
}
/// <summary>
/// Returns whether the instance has been initialized or not.
/// </summary>
public static bool IsInitialized
{
get
{
return instance != null;
}
}
/// <summary>
/// Base awake method that sets the singleton's unique instance.
/// </summary>
public virtual void Awake()
{
if (instance != null)
{
Debug.LogErrorFormat("Trying to instantiate a second instance of singleton class {0}", GetType().Name);
}
else
{
instance = (T) this;
}
}
public virtual void OnDestroy()
{
if (instance == this)
{
instance = null;
}
}
}
I like it, but we will need to make sure everyone calls the base classes when utilizing it. I wish there was a way to make calling the base class required.
You get a warning in the editor if you override Awake without specifying "override". Also, if you don't call base.Awake(), you will get a bunch of null exceptions every time you try to access the instance.
should Awake and OnDestroy be marked as "protected" instead of "public"
What happens if someone doesn't call base.OnDestory()
?
I've also seen this pattern, in the (instance != null)
case in Awake() call DestroyImmediate(this)
just to be sure.
@timGerken Yes it should be protected, the reason it is public is that when this was written at a time where there was a bug in Unity where protected inheritance caused a runtime exception. I'd fix it before submitting :)
If you don't call base.OnDestroy, you have a dangling static reference, which I think will eventually be cleaned up by the garbage collector. Realistically, this is only useful in cases where you want to destroy a Singleton before your app closes. That's already the case with the existing Singleton.
I'm not sure there is a need to call DestroyImmediate in this case, but not strongly opposed to it either.
Should this Singleton pattern survive Unity scene transitions?
If so, in Awake, it should also call DontDestroyOnLoad(this);
@timGerken I think that should be optional. Generally this is a separate script you just attach to your parent object anyway.
@maxouellet I think documenting the required call to the base class should be good enough.
@NeerajW Gaze: It's purposefully not called Gaze because it isn't necessarily tied to gaze. Part of the goal of this design is to be flexible around input mechanisms, so I wouldn't want to strongly tie it to Gaze.
Gesture events: Yes. I think only Navigation was originally missing in there. The reason its missing is because a GestureRecognizer can only support either manipulation or Navigation. I'll likely add Navigation by creating a second GestureRecognizer for that purpose.
Merging ISourceStateHandler and IInputHandler: I don't have strong feelings about that, I think both sides could be argued. I originally made them separate because I'd rather have smaller interfaces than a big interface that forces you to have 5 methods even though you might only care about one of them. This is consistent with Unity's event system interfaces: in fact, they push it even more by having one interface per method.
Voice: This does not aim at addressing voice for now.
@timGerken It's not in there by design, because we've had cases where we only wanted a singleton for a single scene. I haven't played with it much, but what I've typically done is have a "DontDestroyOnLoad.cs" MonoBehaviour that calls DontDestroyOnLoad(this) in Start. That makes it easy to make any GameObject survive scene transitions, without having to add that logic to many of your scripts.
Otherwise you can end up with many singletons calling DontDestroyOnLoad on the same GameObject. Or you can end up with undesired behaviours when reusing components across projects. Making it optional like @HodgsonSDAS suggested would also work if people feel that's really valuable.
@maxouellet sounds good on the GGV discussion. Thanks for considering!
@darax Not knowing Rx is one thing but saying the "var is the opposite of strongly typed" is shocking! This project needs some discussion forum. What about Gitter just like many other .NET project?
@aalmada is right. Technically all of C# is strongly typed anyway, but what we're really trying to say it's not explicit.
In general, if conflicting types can be decided at compile time (resulting in a compiler error), it's strongly typed. If it takes the runtime to discover it (resulting in an Exception), it's weakly typed.
MSDN for Linq Type Relationships:
LINQ query operations are strongly typed in the data source, in the query itself, and in the query execution.
An implicitly typed local variable is strongly typed just as if you had declared the type yourself, but the compiler determines the type.
var i = 10; // implicitly typed
int i = 10; // explicitly typed
I think it's important to keep types explicit for easy readability, easily spot anonymous types, and faster code review during PRs.
@maxouellet and @NeerajW, I strongly agree with moving away from the term gaze to focus. Gaze has a single use target where focus has a broader understanding for multiple targets. Example: targeted interactions from the hand forward or other device. You can allow for multiple "focusers" where "gaze" implicitly has the understanding of originating from the head.
I largely agree with all of the Input changes though the cursor should also be abstracted accordingly. Having an interface -> abstract base class for input sources should also be used for cursors. Trying to model it after the shell cursor from the start doesn't provide the base for other cursors (sprites, material, lights... etc).
All in all awesome work @maxouellet!
@maxouellet any chance we get to play with this over the weekend?
@HodgsonSDAS Merging internal changes with the public version took more time than expected, but I'm back to a state that compiles. I still need to make sure all the tests and example scenes still work, and fix them as appropriate, which I expect is going to take most of today.
I'll try to have some early version (potentially without all example scenes working and with some changes still needed) up on my GitHub account today so that you can play with it. Will update this thread with my progress towards the end of the day.
I've pushed an early first version of this on my GitHub, so that you can play around with it. There's likely still a bunch of broken scenes, mainly references that broke when I deleted / moved / added when I moved from our internal project to the public HoloToolkit project. The best scene to try out the new features is the "InputManagerTest" scene, which shows most of the new functionalities.
I still need to clean up a lot of the code, such as adding the final file header, making the singleton methods protected and adding support for the navigation gesture. I'll also go through all the example scenes to make sure everything works before I send out an official pull request.
One deficiency that I've noticed is the ability to send voice commands to the focused object. I've fixed the test scenes that were doing that, but it's not as clean as I'd like it to be. I'm not 100% sure how to address these scenarios yet, so it will likely be addressed in a future update of that input module.
Overall. Wow, great work! I'm really excited to use this. I do have a few thoughts though.
I noticed in the GazeManager.cs that we're prioritizing hits during the update loop. Should we be using a for loop instead of foreach since this is hot code?
In BaseInputSource.cs in the Events wrappers region the RaiseSourceDetectedEvent
and RaiseSourceLostEvent
raises the opposite event. Is this intentional?
In Button.cs in Awake we set a hash to trigger the dehydrate animation:
gameobject.active(true);
in the animation?gameobject.active(false);
after we dehydrate?InputTest.cs is missing the OnPointerEnter
Debug line.
Other than that I think most everything else is cosmetic, like preferring explicit vars, protected overrides, and extra lines.
BTW, the popup menu test script is really cool. I'd almost say it belongs in the Holotoolkit.
Thanks a lot for the feedback @HodgsonSDAS . Most of what you mentioned are likely bugs, I'll look into them today.
I'm not super familiar with Button.cs, and might end up moving it to the Tests folder because I'm not convinced it's generic enough yet. The intend as I understand it is that the button states (including its active state) is completely controlled by its associated Animator state machine: the script mostly contains logic to toggle between these animations. The hash that is retrieved is just used to optimize modifying modifying the "Dehydrate" parameter on that animator (Unity docs).
One issues that was pointed out by @NeerajW is that InputManager currently allocates the event data classes at each frame. I'll try to take a look at this and see if I can somehow improve this.
@maxouellet Yeah the button class you had looks very similar to the one we had while working on our project at Microsoft earlier this year. We ended up making the hydrate and dehydrate animations completely automated based on the GameObject
's active flag, but still included the option to hydrate in and out independently of the active status. One caveat though is that we had to explicitly call a public method that in turn triggered the dehydrate animation then deactivated the GameObject
after it completed instead of just calling SetActive(false)
.
@maxouellet Tried your version and it's a great improvement. No more script execution order issues.
On the explicit typing, I understand your reasoning but it's arguable: https://blogs.msdn.microsoft.com/ericlippert/2011/04/20/uses-and-misuses-of-implicit-typing/
However, the use of var does have at least the potential to make your code more difficult to understand for other developers. For that reason, the C# documentation generally uses var only when it is required.
-Implicitly Typed Local Variables (C# Programming Guide)
I think it's more important to make the code easier for others to understand because this is Open Source. From my understanding the HTK was initially for newbies who are just starting out in Unity & HoloLens.
I just pushed some more changes to my repository to fix the bugs that @HodgsonSDAS had pointed out, and do the additional fixes:
Last thing I want to do before sending out an official pull request is to take a stab at merging HandsInput.cs and GesturesInput.cs, while would unify hands input with over other Windows-supported type of input.
Really digging that InputManagerTest scene.
There are some issues with the cursor animations though. I was able to trigger the wait animation and then get it stuck on the ring idle by triggering other animations. You may need to reset your triggers before setting them again if the animation gets cut off part way though completion.
I noticed the cursor rotates 180° on the X axis when you gaze off then on the two larger scaled button examples. Is this intentional to show that the cursor will always be visible above the button? If not, the objects cursor modifier normal z should be 0 not -1. The scale of the cursor and the scale of the buttons is misleading. Others may think that the scale of the button is what is affecting the scale of the cursor when it's just the cursor modifier doing all the work.
I noticed you have a snapping mechanism on the popup menu interaction. Should we describe that somewhere in the example as well?
I noticed that the cursor asset you dropped in is the one Greg Bahm made for project Carbon. It still contains animations and geometry specific to that project. You may want to remove the project specific components.
Good points.
We have a better implementation of the cursor coming in soon from @paseb, so the new cursor on my GitHub is really just a stand-in. I'll take a quick look at cleaning it up in the meantime, but odds are it simply won't be part of the pull request.
We could add some description in the example scene, but it's already pretty heavy on text. It's meant as a demo that shows the various capabilities of the input system and the cursor system, so it's kinda by design that users should explore the scene and look at the GameObjects to understand the behaviours they might be interested in having in their own project. My suggestion would be to leave it as-is for now, and we can always eventually add a ReadMe.md or something like that as necessary.
@maxouellet you also have two extra meta files that can be deleted from your repo. The Editor complains when I switch over to your branch each time, saying they've been deleted because there aren't any assets they're related to.
Removing Assets/ThirdParty/HoloToolkit/Input/Models/Cursor/cursor_hand_ready.fbm/templates because the asset does not exist
Removing Assets/ThirdParty/HoloToolkit/Input/Models/Cursor/cursor_ready.fbm/templates because the asset does not exist
Thanks for the notice, I pushed a change that removes these two files.
The input stuff is pretty much done at this point, minus the cursor. Goal is to get the cursor in today or tomorrow at latest, and send out a pull request once that's done. I also need to update the documentation, which I will do today.
@maxouellet You don't need so many delegates for the input sources. Two are enough and you don't even need to declare them. Use Action<> in the event declaration as suggested in the .NET design guidelines for callbacks:
event Action<IInputSource, uint> SourceUp; event Action<IInputSource, uint, Vector3> ManipulationStarted;
These don't follow the .NET design guidelines for events but it would make the sender not strongly-typed.
@aalmada In this specific case, I'm not a big fan of using Action, because it hides the meaning of the uint. I can certainly change it if people feel it's more obvious when using Action.
It agree that EventHandler and event args for the various events would be better overall, but I was trying to avoid the allocation of an *EventArgs object instance on every interaction event, given that this will be running in Unity on HoloLens. I'm open to suggestions on this
@maxouellet Come on! Everything that is not C# 1.0 is not readable? Generics is 2.0. C# is now 6.0 and Unity supports up to 4.0. At least you could reduce to only two delegates...
This is the initial suggested design for the new input module. Feel free to ask questions / request changes.
I already have a version of this implemented, to which I will make improvements as needed. Goal is to add it to HoloToolkit so that new features can be added by the community as needed. It has been designed to be as extensible as possible.
Why design a new Input Module?
A lot of applications have a need for an efficient way to handle input. In an ideal world, game objects can handle the input events they are interested in without having to write the same code in multiple components. The current solution in HoloToolkit isn't very performant and relies on Unity's messaging system, which is not very performant and doesn't make it obvious that behaviours are handling input events. It also doesn't extend well to support multiple different input mechanisms.
Unity provides an event system that it currently leverages to send out inputs to game objects. It contains a HoloLensInputModule that interprets HoloLens input and sends out the standard Unity events. The solution I propose does not use this input module, for the following reasons:
The proposed design leverages Unity's event system, but implements its own version of an input module (called InputManager in this case). This currently gives us more flexibility in exploring various interaction mechanisms, which is very helpful when working with novel inputs devices such as what HoloLens provides.
Class diagram
Overall view of the main classes and interfaces that make up the initial version of this input module. Minor changes could still be done before submission (for example, HandsInput will likely be merged with GesturesInput, as their functionalities are very similar).
Input Module Design
The input module is designed to be extensible: it could support various input mechanisms and various types of gazers.
Each input source (hands, gestures, others) implement a IInputSource interface. The interface defines various events that the input sources can trigger. The input sources register themselves with the InputManager, whose role it is to forward input to the appropriate game objects. Input sources can be dynamically enabled / disabled as necessary, and new input sources can be created to support different input devices.
Game objects that want to consume input events can implement one or many input interfaces, such as:
The input manager listens to the various events coming from the input sources, and also takes into account the gaze. Currently, that gaze is always coming from the GazeManager class, but this could be extended to support multiple gaze sources if the need arises.
By default, input events are sent to the currently focused game object, if that object implements the appropriate interface. Modals input handlers can also be added to the input manager: these modal handlers will take priority over the currently focused object Fallback handlers can also be defined, so that the application can react to global inputs that aren't targeting a specific element. Any event sent by the input manager always bubbles up from the object to its ancestors.
In recap, the input manager forwards the various input sources events to the appropriate game object, using the following order:
The input manager also has a pointer to the currently active Cursor, allowing it to be accessed from there. The cursor currently also depends on the gaze coming from the GazeManager class.