Thoughts on this player interact method?

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.

wow thanks for the feedback everyone!

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.

2 Likes

Thank you I appreciate the feedback, and the forum advice as well.

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.

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.

1 Like

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.

    public void CheckForInteraction() {
       
        if (CanInteract()) {
            // can interact
            if (!isCurrent) {
                // is not current
                managerInstance.SetCurrentInteractObject(this);
                isCurrent = true;
            }
        } else if (isCurrent) {
            // is current, and can no longer interact
            managerInstance.RemoveCurrentInteraction();
            isCurrent = false;
        }
    }

    bool CanInteract() {
        // check if wihin range
        if (Vector3.Distance(GetPlayerPos(), m_Trans.position) >= interactDistance) return false;
        DebugPrint("is within range");
        // Shoot a ray from the camera's center
        Ray ray = Camera.main.ScreenPointToRay(new Vector3(Screen.width / 2, Screen.height / 2, 0));
        // Declare a variable to store information about the hit
        RaycastHit hit;
        // Check if the ray hits something within the specified max distance and on the interaction layer
        if (Physics.Raycast(ray, out hit, maxRaycastDistance)) {
            DebugPrint("Ray hit");
            // The ray hit an object
            if (hit.collider.gameObject.name == interactCollider.name)  { DebugPrint("can interact"); return true; }
        }

        DebugPrint("cant interact");
        return false;
    }

    public virtual void RunInteract() {
        if (lockPlayer) {
            player.SetCanMove(false);
            playerTransform.parent = transform;
            player.SetCharacterCotrollerActive(false);
        }
    }

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:

    public void CheckForInteraction() {
        Ray ray = Camera.main.ScreenPointToRay(new Vector3(Screen.width / 2, Screen.height / 2, 0));
        // Declare a variable to store information about the hit
        RaycastHit hit;
        // Check if the ray hits something within the specified max distance and on the interaction layer
        if (Physics.Raycast(ray, out hit, maxRaycastDistance)) {
            // Ray hit
            // DebugPrint("Ray hit");
            // attempt to retrieve an interactable component from the collide that was hit
            var interactObj = hit.collider.GetComponentInParent<Interactable>();
            if (interactObj != null) {
                // the cam is looking at an interact component
                if (Vector3.Distance(playerTransform.position, interactObj.GetPosition()) <= interactObj.interactDistance) {
                    // is within range
                    SetCurrentInteractObject(interactObj);
                    // DebugPrint("did set the current object to: " + currentObj);
                } else RemoveCurrentIfExists(); // ray hit interactable, but the player is not within range
            } else RemoveCurrentIfExists(); // ray hit, but not an interactable
        } else RemoveCurrentIfExists(); // ray did not hit anything
    }

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?

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.

2 Likes