Search Unity

  1. We are migrating the Unity Forums to Unity Discussions. On July 12, the Unity Forums will become read-only. On July 15, Unity Discussions will become read-only until July 18, when the new design and the migrated forum contents will go live. Read our full announcement for more information and let us know if you have any questions.

Discussion Thoughts on this player interact method?

Discussion in 'Input System' started by angeldevelopment, Oct 31, 2023.

  1. angeldevelopment

    angeldevelopment

    Joined:
    Sep 28, 2022
    Posts:
    247
    So im writing this at 2 am and im four melatonin gummies deep buttttttt

    Im writing my first interaction system, where the player can interact with objects. I know there are a lot of solutions out there, but I wanted tot ry out my own. Basically this is how it works.

    I have a parent class called Interactable, which has a public Collider, and a virtual function called RunInteract(), as well as an OnTriggerStay() override.

    Then any of my objects, for example, DoorController, inherit from Intractable. When the player enters the trigger of an object that inherits from interactable, the OnTriggerStay() function checks if the main camera is looking at the objects Collider, (which has a public reference mentioned at the beginning) via a raycast, if this raycast hits the collider, then this means that the player is both within the trigger and looking at the object.

    My player class has a shared instance, and a method that sets the current Interactable object. This method, Player.Instance.SetCurrentInterableObject(Interactable i) gets called from the Interactable object that determined the player was looking at it.

    Finally if the user hits the interact button, then the player class calls the method on the current interact object, currentInteractable.RunInteract() which calls the virtual method described in the beggining, which all of my doors, and tables etc. will override.

    Seems pretty complicated, but the only real cost I see if a ray cast per frame. Lmk what you guys think.
     
  2. angeldevelopment

    angeldevelopment

    Joined:
    Sep 28, 2022
    Posts:
    247
    wow thanks for the feedback everyone!
     
  3. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,647
    Well you posted this in the Input System subforum when this post has nothing to do with said input system; so not really surprising it was overlooked.

    But what you suggest is a pretty common overall approach in broad strokes. Though rather than an Interactable base class, I would have an
    IInteractable
    interface instead, as this is something better off composed across a number of different objects that may have their own wildly different implementations.

    Probably don't need the trigger either. Just test if the player is looking at an interactable within a certain range. It's not going to be a performance concern.
     
  4. angeldevelopment

    angeldevelopment

    Joined:
    Sep 28, 2022
    Posts:
    247
    Thank you I appreciate the feedback, and the forum advice as well.
     
  5. angeldevelopment

    angeldevelopment

    Joined:
    Sep 28, 2022
    Posts:
    247
    Why the interface tho, it seems useful to use inheritance since, for instance, i will have. sound play when the user interacts with the object, and so the class Intractable has a public audio source/clip to play, amongst other exposed fields.
     
  6. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,647
    What you're talking about is akin to a subclass sandbox: https://gameprogrammingpatterns.com/subclass-sandbox.html

    Which has it's benefits but it only really works if you have a very narrow scope potential implementations, or are using the sandbox for a group of intrinsically related derived types (like superpowers as shown in the link).

    But going down this route, you will very likely find that you will be colliding with the need for some objects to inherit from another more relevant type, or multiple types (which C# can't do). Such as an explosive barrel that wants to be both interactable (pick it up), and damageable, and potentially other things (magnetic?).

    Inheritance can't accomodate for this easily, not without making an absolute mess of your code. However, you can just express IInteractable, IDamageable, IMagnetic interfaces to allow said barrel to be all three without it needing to rigidly inherit from a specific class.

    Any common functionality can be handled by utility classes, whether that be static classes or small, encapsulated objects.

    You'll hear this advice a lot as it just ultimately ends up being more flexible and extensible when it comes to games.
     
    angeldevelopment likes this.
  7. angeldevelopment

    angeldevelopment

    Joined:
    Sep 28, 2022
    Posts:
    247
    So I have stuck with the inheritance structure because, for me, it is more intuitive, and also, if I use interfaces I will have to rewrite all lot of the code, which is the whole reason Im using these classes anyway.

    For example, weapon inherits from item. Item in inherits from interactable. If i use interfaces, then the code that gets called when an item is picked up, I will have to rewrite for the weapon class. Weapon having its own implementing of the method does not help me.

    But maybe I am not seeing it correctly. Anyways I was using this code on each Interactable object.

    Code (CSharp):
    1.     public void CheckForInteraction() {
    2.        
    3.         if (CanInteract()) {
    4.             // can interact
    5.             if (!isCurrent) {
    6.                 // is not current
    7.                 managerInstance.SetCurrentInteractObject(this);
    8.                 isCurrent = true;
    9.             }
    10.         } else if (isCurrent) {
    11.             // is current, and can no longer interact
    12.             managerInstance.RemoveCurrentInteraction();
    13.             isCurrent = false;
    14.         }
    15.     }
    16.  
    17.     bool CanInteract() {
    18.         // check if wihin range
    19.         if (Vector3.Distance(GetPlayerPos(), m_Trans.position) >= interactDistance) return false;
    20.         DebugPrint("is within range");
    21.         // Shoot a ray from the camera's center
    22.         Ray ray = Camera.main.ScreenPointToRay(new Vector3(Screen.width / 2, Screen.height / 2, 0));
    23.         // Declare a variable to store information about the hit
    24.         RaycastHit hit;
    25.         // Check if the ray hits something within the specified max distance and on the interaction layer
    26.         if (Physics.Raycast(ray, out hit, maxRaycastDistance)) {
    27.             DebugPrint("Ray hit");
    28.             // The ray hit an object
    29.             if (hit.collider.gameObject.name == interactCollider.name)  { DebugPrint("can interact"); return true; }
    30.         }
    31.  
    32.         DebugPrint("cant interact");
    33.         return false;
    34.     }
    35.  
    36.     public virtual void RunInteract() {
    37.         if (lockPlayer) {
    38.             player.SetCanMove(false);
    39.             playerTransform.parent = transform;
    40.             player.SetCharacterCotrollerActive(false);
    41.         }
    42.     }
    Basically each interactable object checks its distance from the player, if It is in range, a ray cast is shot to determine if the player is looking at the object. But ehrre is 2 problems with this.

    1. If the player is near a tons of objects, there could be a lot of raycasts at once.
    2. (the real problem) if two gameobjects have the same name, this will cause issues, and it did. If the player is near 2 doors, with the same name, then both will get set to the current object in the same frame.

    I also dont love the idea of every single interactable object in the scene calling a function to check if its close to the player, it seems messy. So instead I have written this function that is called by InteractManager:

    Code (CSharp):
    1.     public void CheckForInteraction() {
    2.         Ray ray = Camera.main.ScreenPointToRay(new Vector3(Screen.width / 2, Screen.height / 2, 0));
    3.         // Declare a variable to store information about the hit
    4.         RaycastHit hit;
    5.         // Check if the ray hits something within the specified max distance and on the interaction layer
    6.         if (Physics.Raycast(ray, out hit, maxRaycastDistance)) {
    7.             // Ray hit
    8.             // DebugPrint("Ray hit");
    9.             // attempt to retrieve an interactable component from the collide that was hit
    10.             var interactObj = hit.collider.GetComponentInParent<Interactable>();
    11.             if (interactObj != null) {
    12.                 // the cam is looking at an interact component
    13.                 if (Vector3.Distance(playerTransform.position, interactObj.GetPosition()) <= interactObj.interactDistance) {
    14.                     // is within range
    15.                     SetCurrentInteractObject(interactObj);
    16.                     // DebugPrint("did set the current object to: " + currentObj);
    17.                 } else RemoveCurrentIfExists(); // ray hit interactable, but the player is not within range
    18.             } else RemoveCurrentIfExists(); // ray hit, but not an interactable
    19.         } else RemoveCurrentIfExists(); // ray did not hit anything
    20.     }
    This seems much cleaner to me. Instead of every single object checking its distance, the manager shoots a ray. Attempts to retrieve a Interactable component from the collider it hits, then checks the distance, and sets if within range. The only downside to this is the GetComponent<>() call, but this ensures that the correct interactable object is set, and only shoots 1 ray per frame. Thoughts?
     
  8. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,647
    Yes a single system checking for interactable objects is definitely how you should approach it. Really it just needs to be whatever the player is looking at. A GetComponent check or two per-frame is a non-issue.

    Yes you have found yourself with technical debt due to inheritance. You've got multiple layers of inheritance so now there's too much dependency, making it hard to change anything.

    Inheritance is really about substitution. One object should be able to be substituted for another, which is the purpose of polymorphism. While it can be about reusing functionality, this shouldn't be the major driver for using it.

    Ideally, particularly in game dev, your inheritance chains should only be 1-2 deep. Shallow but wide is best. And as the saying goes, composition over inheritance.

    Were you to use interfaces, an object implementing IInteractable could be used for picking up an item, opening a door, anything really. They can all express slightly different implementations pretty freely without incurring technical debt on anything else.

    And not to say you can't use interfaces with a base class or reusable implementation, particularly when you have a situation where a number of objects will do the same or similar things.

    Because lets be real, items and weapons don't need to have their own implementation of being picked up. A reusable "Item Pickup" or whatnot component can do this. Then weapons don't have to worry about that at all.

    Food for thought. I use both interfaces with inheritance together quite a lot. Can't make flexible systems without them.
     
    angeldevelopment and Kurt-Dekker like this.