Use the current room’s physicalObjects groups to find realized items that can interact with physics now. This recipe returns the nearest loose PlayerCarryableItem whose chunk is visible from the player. It performs one bounded scan and does not retain the result.

Scan every collision layer

using UnityEngine;
 
private static PlayerCarryableItem FindNearbyItem(Player player, float radius)
{
    if (player == null || player.room == null || player.mainBodyChunk == null ||
        player.room.physicalObjects == null || radius <= 0f)
        return null;
 
    Room room = player.room;
    Vector2 origin = player.mainBodyChunk.pos;
    float bestDistanceSquared = radius * radius;
    PlayerCarryableItem nearest = null;
 
    for (int layer = 0; layer < room.physicalObjects.Length; layer++)
    {
        for (int index = 0; index < room.physicalObjects[layer].Count; index++)
        {
            var item = room.physicalObjects[layer][index] as PlayerCarryableItem;
            if (item == null || item.room != room || item.slatedForDeletetion ||
                item.grabbedBy.Count != 0 || item.bodyChunks == null)
                continue;
 
            for (int chunkIndex = 0; chunkIndex < item.bodyChunks.Length; chunkIndex++)
            {
                BodyChunk chunk = item.bodyChunks[chunkIndex];
                if (chunk == null)
                    continue;
 
                float distanceSquared = (chunk.pos - origin).sqrMagnitude;
                if (distanceSquared < bestDistanceSquared &&
                    room.VisualContact(origin, chunk.pos))
                {
                    bestDistanceSquared = distanceSquared;
                    nearest = item;
                }
            }
        }
    }
 
    return nearest;
}

Call it from a player event after checking the player state, for example:

PlayerCarryableItem item = FindNearbyItem(self, 80f);
if (item != null)
    Logger.LogInfo("Nearby item: " + item.abstractPhysicalObject.type.value);

global::Room.physicalObjects is grouped by collision layer. Searching only one inner list can miss an eligible item in another layer. global::Room.updateList also contains effects and other updated objects, while global::AbstractRoom.entities can contain abstract objects with no realized body. The physics groups match this task.

The method measures each body chunk because an item with several chunks can cross the radius while its first chunk remains outside it. Squared distances avoid a square root. The strict < comparison excludes a chunk exactly on the radius. VisualContact(Vector2, Vector2) adds a terrain line check. Remove that condition when walls should not matter.

Use the result immediately

Room collections can change when objects are added, removed, grabbed, abstractized, or moved between rooms. Finish the scan before changing any candidate. Before applying a later action, check item.room == player.room, deletion state, and the action’s own conditions again. A saved PlayerCarryableItem reference can become stale after room unloading. Abstract and realized objects covers that transition.

For a specific feature, narrow the type and state tests. Object interaction shows a selection limited to rocks, with weapon mode checks and a cosmetic marker. Test empty rooms, several collision layers, items with several chunks, exact radius, blocked sight, grabbed items, room transitions, and cooperative players. The helper compiled against the recorded baseline with .NET SDK 10.0.400 and C# 7.3. It was not run in the game.

Sources

  • global::Room.physicalObjects, global::Room.VisualContact(UnityEngine.Vector2,UnityEngine.Vector2), global::PhysicalObject.bodyChunks, global::PhysicalObject.grabbedBy, and global::UpdatableAndDeletable.slatedForDeletetion in the baseline assembly recorded by Runtime reference.