6

I'm adding a replaying feature to my game. Having this feature, I can capture user's input to the game and feed them back to Unity later on for replaying the game. But the design of VRStandardAssets.Utils.VRInput class prevents me to mimic user inputs.

This class does not provide any public method to make it possible to trigger its events (e.g. OnClick or OnDoubleClick events) programmatically. So I decided to create a derived class from it and write my own public methods to trigger the events. This strategy failed because VRInput's methods are private meaning that I cannot invoke them from a derived class.

It is recommended for this type of classes to provide a protected virtual void On[eventName](subclassOfEventArgs e) method to provide a way for a derived class to handle the event using an override but this class does not have it (why so restrictive?). I guess it's a poor design from Unity. This poor design also makes it hard to write unit/integration tests.

Am I missing something here? Can I still do something to trick other classes to think they are dealing with VRInput class while replaying the game?

Ludovic Feltz
  • 11,416
  • 4
  • 47
  • 63
Kamran Bigdely
  • 7,946
  • 18
  • 66
  • 86
  • 1
    Writing game apps is different than writing standard application. Many works flows are abandoned to make things easier or faster. They simply want you to subscribe to the events which is totally fine. The events are also public which means you can easily invoke them from your script. I don't think they want you to derive from `VRInput` because it create multiple instances of `VRInput` which is unnecessary since there is only one user input. Making it worth being derived from would have been fine if this was a UI control like a UI button event since there can be many of them in the scene. – Programmer Apr 16 '18 at 20:10
  • Can you please elaborate on "The events are also public which means you can easily invoke them from your script"? Events are commonly defined as public but that does not mean other classes can 'invoke' them. They can just subscribe to them. – Kamran Bigdely Apr 16 '18 at 20:21
  • Find VRInput GameObject: `GameObject obj = GameObject.Find("VRInputObj");` . Now get the VRInput component from it: `VRInput vrInput = obj.GetComponent();` then invoke any of its event: `vrInput.OnUp != null) { vrInput.OnUp.Invoke(); }` – Programmer Apr 16 '18 at 20:37
  • It's not made to me invoked manually. They didn't have that in mind. That API is just a basic minimum VR stuff required to just made a **simple** VR game in your scene without any plugin. If you want a complicated Input system, you should make your own or use a existing one like GoogleVr. – Programmer Apr 16 '18 at 20:47
  • @Programmer: vrInput.OnUp.Invoke() does not compile. OnUp is an event. You can only use it to subscribe to it. It gives an error: "The event 'VRInput.OnClick' can only appear on the left hand side of += or -= (except when used from within the type 'VRInput')" – Kamran Bigdely Apr 16 '18 at 20:53
  • I used the `VRInput` script you linked in your question to provide this example. Are you sure that's the-same one you are using? If yes then add EDIT to your question and add the code you are using with the error you are getting. You are likely doing something wrong. – Programmer Apr 16 '18 at 20:59
  • @Programmer: Yes. It's the same class. Please note that "Outside of the class it is defined in, an event can only add or subtract references." Here is the code somewhere in my GameManager script: Camera.main.GetComponent().OnClick.Invoke(); – Kamran Bigdely Apr 16 '18 at 21:04
  • Sorry for wasting your time. You can't call event from another class. Like I said before I don't they want you to be able to do that. If you still want to do it, create a new VRInput script. Copy the old original code code inside it then delete the original VRInput script. Add a public function that will let you invoke the methods in your new script. – Programmer Apr 16 '18 at 21:16
  • If I copy over the code and create my own version of VRInput, then I have to create my own version of VREyeRaycaster and any other classes that uses VRInput class. It's doable but It's just not design-pattern-friendly. Thanks anyway. – Kamran Bigdely Apr 16 '18 at 21:30
  • 1
    No you don't have to. Give it the-same class name `VRInput`. Put it in the `VRStandardAssets.Utils` namespace then drop it in the location the old one is already at. That's it. you can even use this time to add the `virtual` method you've been talking about. – Programmer Apr 16 '18 at 23:59

2 Answers2

2

In fact you can trigger theses events (OnClick, OnDoubleClick or any other events) from another class and without using reflection using this clever hack (Inspired by this article):

C# is not really type safe so you can share the same memory location. First declare a class with two fields that share the same memory space:

[StructLayout(LayoutKind.Explicit)]
public class OverlapEvents
{
    [FieldOffset(0)]
    public VRInput Source;

    [FieldOffset(0)]
    public EventCapture Target;
}

Then you can declare a new class that will intercept and call the other event:

public class EventCapture
{
    public event Action OnClick;

    public void SimulateClick()
    {
        InvokeClicked();
    }

    // This method will call the event from VRInput!
    private void InvokeClicked()
    {
        var handler = OnClick;
        if (handler != null)
            handler();
    }
}

Then finally register it and call it:

public static void Main()
{
    input = GetComponent<VRInput>();

    // Overlap the event
    var o = new OverlapEvents { Source = input };

    // You can now call the event! (Note how Target should be null but is of type VRInput)
    o.Target.SimulateClick();
}

Here is a simple dotNetFiddle that show it working (at least outside of unity)

Ludovic Feltz
  • 11,416
  • 4
  • 47
  • 63
0

It is recommended for this type of classes to provide a protected virtual void On[eventName](subclassOfEventArgs e) method to provide a way for a derived class to handle the event using an override but this class does not have it (why so restrictive?). I guess it's a poor design from Unity. This poor design also makes it hard to write unit/integration tests.

All code is good code. All code is also bad code. Depends on your evaluation criteria. Unity's developers probably didn't think about your use case. As another conflicting rule of thumb, software should also be as simple & rigid as possible, so anticipating subclassing without a known use case might be considered overengineering.

As for how you can work around this, see How do I raise an event via reflection in .NET/C#?

Warty
  • 7,237
  • 1
  • 31
  • 49