0
\$\begingroup\$

I have a Unity 2D project which has a player character and numerous NPCs all potentially moving (top-down) at the same time during play. The number of NPCs is not to exceed 100 (and will be, probably, more like 40-50) but I'd still like to be performant.

The "rooms" are really just rectangles that correspond to sections of my tilemap. All the "rooms" are contiguous, and hence all characters will always be in exactly 1 "room". Like if you picture the floorplan of a house, you're in the "Hallway" or the "Living Room" or the "Front Porch" or the "Driveway", and so on.

My tilemap is not complete, so it would be nice to be able to easily move the rooms around in the Scene editor as I refine it.

I was thinking I could...

  1. Create Rect objects (or maybe an AABE, although I just read very briefly about what this even is) for each room inside a "RoomsController" GameObject, and then on each Update() iterate through each and see who is within the bounds of each room. But that seems majorly wasteful, and pretty brute-force even if done every few frames.
  2. Create a GameObject for each room with an appropriately-sized BoxCollider 2D, and look for collisions with NPCs, and have a script that handles changing the active room on the NPC when he "collides" with a new room. But this requires a RigidBody 2D for each room, and it feels like overkill.

Is there an existing "right way" to solve this problem?

\$\endgroup\$
0
\$\begingroup\$

I would define a class for each type of room shape. For rooms that are quadrilaterals or circles, it is easy to define those shapes and test if a given point is within them. If the shapes of your rooms are anything else, it may be easier to create a Polygon Collider and test Collider2D.OverlapPoint using that collider and the position of the unit:

public class Room 
{
    public virtual bool IsInRoom(Vector2 point)
    {
        return false;
    }

    public class Quad : Room
    {
        public Vector2 min {get; private set;}
        public Vector2 max {get; private set;}
        public Quad(Vector2 min, Vector2 max)
        {
            this.min = min;
            this.max = max;
        }

        public override bool IsInRoom(Vector2 point)
        {
            if (point.x > min.x && point.x < max.x)
                if (point.y > min.y && point.y < max.y)
                    return true;
            return false;
        }
    }

    public class Circle : Room
    {
        public Vector3 center {get; private set;}
        public float radius {get; private set;}
        public Circle(Vector3 center, float radius)
        {
            this.center = center;
            this.radius = radius;
        }

        public override bool IsInRoom(Vector2 point)
        {
            if (Vector2.Distance(point, center) > radius)
                return true;
            else 
                return false;
        }
    }

    public class Polygon : Room
    {
        public Collider2D collider {get; private set;}
        public Polygon(Collider2D collider)
        {
            this.collider = collider;
        }

        public override bool IsInRoom(Vector2 point)
        {
            return collider.OverlapPoint(point);
        }
    }
}

You could make use of the [System.Serializable] tag and OnDrawGizmos in order to edit these rooms from within the inspector.

After populating the list of rooms, a foreach loop could iterate through the rooms and units, testing each one:

foreach (Unit unit in units)
    foreach (Room room in rooms)
    {
        if(room.IsInRoom(unit.position))
        {
            unit.currentRoom = room;
            break;
        }
    }

I didn't test this code, so you may need to make minor changes to it for syntax/optimization. Good luck!


EDIT: I would like to answer some questions left in the comments to this question.

Should the Room actually inherit from MonoBehaviour, and be a script attached to a set of GameObject that I position, or is it expected that these would be initialized by another script somewhere?

What I like to do in my games is have an object in my scene with some manager scripts attached to it. For example, I have an AudioSource and a script called SFXManager attached to it, and SFXManager holds a list with all the different sound effects in the game that are assigned in the inspector, then at runtime those clips are put into a static Dictionary<string, AudioClip> that I can reference publicly.

The manager I would create in your scenario would store a list of all the different Rooms in your game, but since these different types of rooms all inherit from the base class Room, I'm unsure of how exactly you would go about editing the room values in the inspector since the inspector won't know which type of room a given item in a List<Room> is. So, to backtrack a bit, I would separate the different room types into their own scripts:

public class Room : MonoBehaviour
{
    public string name;
    private void Awake()
    {
        MapManager.rooms.Add(this);
    }
    public virtual bool IsInRoom(Vector2 point)
    {
        return false;
    }
}

public class Quad : Room
{
    public Vector2 min;
    public Vector2 max;

    public override bool IsInRoom(Vector2 point)
    {
        if (point.x > min.x && point.x < max.x)
            if (point.y > min.y && point.y < max.y)
                return true;
        return false;
    }

    void OnDrawGizmos()
    {
         // draw the room
    }
}

public class Circle : Room
{
    public Vector3 center;
    public float radius;

    public override bool IsInRoom(Vector2 point)
    {
        if (Vector2.Distance(point, center) > radius)
            return true;
        else 
            return false;
    }

    void OnDrawGizmos()
    {
         // draw the room
    }
}

public class Polygon : Room
{
    public Collider2D collider;

    public override bool IsInRoom(Vector2 point)
    {
        return collider.OverlapPoint(point);
    }

    void OnDrawGizmos()
    {
         // draw the room
    }
}

Here, I've maintained the inheritance, and had the base Room class inherit from MonoBehaviour since C# doesn't allow multiple inheritance. I've also changed the variables from properties to fields to allow inspector editing. These scripts would simply be attached to objects in your scene, allowing their Awake() function to be called which adds them to the static List<Room> we will store in our manager. Units will need to follow the same idea:

public class Unit : MonoBehaviour 
{
    private void Awake()
    {
        MapManager.units.Add(this);
    }
}

Now, we can create our MapManager:

public class MapManager : MonoBehaviour
{
    public static MapManager ins {get; private set;}
    public static List<Room> rooms {get; private set;}
    public static List<Unit> units {get; private set;}

    private void Awake()
    {
        if (ins != null) Destroy(ins);
        ins = this;
        rooms = new List<Room>();
        units = new List<Unit>();
    }

    public Room GetRoomOfUnit(Unit unit) 
    {
        foreach (Room room in rooms) 
            if (room.IsInRoom(unit.transform.position))
                return true;

        return null;
    }

    public Unit[] GetUnitsInRoom(Room room)
    {
        List<Unit> units = new List<Unit>();
        foreach (Unit unit in this.units)
            if (room.IsInRoom(unit.transform.position))
                units.Add(unit);

        return units.ToArray();
    }
}

It's important to note that script execution order needs to be changed so that MapManager executes before both Room and Unit. Otherwise, MapManager.rooms and MapManager.units will both be null when the Room.Awake() and Unit.Awake() functions are called, resulting in a NullReferenceException.


It seems like it would be easier to do maybe a Physics2D.OverlapBoxAll() somehow for the room, and check for colliders within my quad. But I'm not clear if (a) this -- or your Polygon implementation -- necessitates having RigidBodys for everything, and (b) what is more performant, academically, versus looping through each room for each character until it's found?

None of the Physics.Overlap____ methods require Rigidbody components to be attached to the objects with the colliders. The Rigidbody component is for handling physics - which we aren't really doing here. We're simply using the colliders as a means to bypass the math involved in determining the intersections of 3D shapes. Since i assume there are more units than rooms, and the only complicated maths would come from irregularly shaped rooms, it makes more sense to only have colliders attached to irregularly shaped rooms, and to simply test the world position of each unit against the bounds of each room.

As to the second part of your question, you can have multiple methods of searching. For example, if you want to know which room a given unit is in, you only need to loop through the list of rooms, testing the given unit against each of them. If you want to know all of the units inside a given room, it is going to be a bit more expensive, but depending on how many units you have at a given moment, it's probably going to be cheaper to just call Room.IsInRoom() hundreds of times than having to redesign the system to work where every frame each unit checks for collisions with colliders you would then need to attach to each room.

|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ Thank you so much. I apologize that I didn't see my notification to your response. This is wonderful. One question I have: should the Room actually inherit from MonoBehaviour, and be a script attached to a set of GameObjects that I position (especially since you refer to using OnDrawGizmos)? Or is it expected that these would be initialized by another script somewhere? It seems daunting to position them properly without a GUI, but I think the aforementioned would solve this. \$\endgroup\$ – Offlein Jan 12 at 4:39
  • \$\begingroup\$ Oh! And another two questions, I think. it seems like it would be easier to do maybe a Physics2D.OverlapBoxAll() somehow for the room, and check for colliders within my quad. But I'm not clear if (a) this -- or your Polygon implementation -- necessitates having RigidBodys for everything, and (b) what is more performant, academically, versus looping through each room for each character until it's found? Thanks again! \$\endgroup\$ – Offlein Jan 12 at 4:43
  • \$\begingroup\$ I'll edit my answer with answers to your follow-up questions. \$\endgroup\$ – Agent Tatsu Jan 14 at 19:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.