Möbel Finder: Mastering 3D Drag-and-Drop in Unity (Part 1 / 2)

Yann K.Yann K.
9 min read

I. Introduction and Project Overview

1.1 Technical Specifications

Engine: Unity
Language: C#
Platform: PC
Team Size: Solo Play
Development Time: 3 days
Status: Published

1.2 Game Definition and Mechanics

Möbel Finder (German for "Furniture Finder") combines the fun of a casual puzzle with the stress of time-management. It's a light but strategic game that tests players' accuracy and speed as they drag and drop furniture into the right places in a race against the clock.

Each level starts with the furniture in the same spot. As a player you get a time limit and a list of the furnitures to move. When you drag a piece of furniture from the list to its corresponding spot, it disappears from the list. If you put something in the wrong spot, it just goes back to its initial place.


II. Research Focus and Objectives

2.1 Primary Developments Goals

Focus of Development: Fundamental Drag-and-Drop System

Main Goals

  • Make dragging and dropping items easy for gameplay.

  • Ensure accurate placement with clear visual feedback.

  • Add a timer to create some time pressure for success or failure.

2.2 Achievement Summary

What I Accomplished

  • Incorporated reliable 3D object selection and dragging

  • Created a plane-constrained mobility system

  • Solved cursor-speed drag interruption issues

  • Added visual highlighting for interactive objects

  • Drop zone validation (basic implementation complete)


III. Technical Implementation and Analysis

3.1 Core Challenge Definition

I had to try different methods to overcome some technical obstacles to create a responsive, smooth drag-and-drop mechanic in 3D space.

3.2 Object Selection & Hover Feedback System

3.2.1 Raycast Detection Implementation

Raycast Detection:

// Detect furniture objects under cursor
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit, _rayDistance, _furnitureMask)) 
{
    _selectedGO = hit.collider.gameObject;
    _renderer = _selectedGO.GetComponent<MeshRenderer>();
}
  • Purpose: It checks for furniture items under the cursor with a layer mask called _furnitureMask.

3.2.2 Visual Highlight System

It changes the object (and its child objects) to red when you hover over it.

// Highlight selected object and children
_renderer.material.color = Color.red;
foreach (Transform child in selectedObject.transform)
{
    var childRenderer = child.GetComponent<MeshRenderer>();
    if (childRenderer != null)
        childRenderer.material.color = Color.red;
}
  • Note: It uses a list (_listRenderer) to cache child renderers for better performance.

3.3 Drag Mechanics: Itereative Development Process

3.3.1 Approach 1: Dual-Raycast Drag-system

In this implementation, two separate raycasts team up:

Furniture Selection Raycast

  • Layer Mask: _furnitureMask

  • Purpose: It identifies which furniture object is being interacted with

      if (Physics.Raycast(ray, out hit, _rayDistance, _furnitureMask)) {
          // Highlight and select furniture object
          _selectedGO = hit.collider.gameObject;
      }
    

Ground Positioning Raycast

  • Layer Mask: _groundMask

  • Purpose: It determines where to place the selected furniture and updates position during drag

              _dragOffset = _selectedGO.transform.position - groundHit.point;
              _selectedGO.transform.position = hit.point + _dragOffset;
    

Why Two Separate Raycasts?

  • The furniture raycast handles selection (what you're dragging)

  • The ground raycast handles placement (where you're dragging it to)

  • The offset helps keep the object at the right height above the ground

3.3.2 Problem Identification: Drag Interruption

When you are smoothly guiding your mouse across the screen, everything is great until you speed up a bit. The object that you were moving stops suddenly, even though you're still holding the mouse button down. Any idea why?

It's the raycast. You see, when you drag slowly, the raycast keeps hitting the object, which is good. However, if you move too fast, the cursor flies off the object so fast that the system can’t keep up. The raycast loses track of it, and your drag stops, even though you're still holding down the mouse button.

[MOUSE MOVEMENT]the selection depended on constantly detecting the hover, even when you were actively dragging
     ↓
[Furniture Raycast] → Selects object (what to move)
     ↓
[Ground Raycast]    → Finds placement point (where to move)
     ↓
[Offset Calculation] → Maintains consistent height

3.3.3 Approach 2: Screen-to-World Conversion

Here i try to fix the dragging issue by keeping the object's depth while tracking the mouse mouvement.

The problem:

To drag a 3D object, the mouse position must be changed from the 2D back into 3D space. The catch is that the mouse only shows X and Y coordinates, which means that there's nothing for depth.

The solution:

a. Get the Depth First: When selecting an object, it is checked how far the object is from the camera, mainly the Z-position.

b. Save the new position: A new position is created using the mouse's current X and Y coordinates along with the previous saved depth.

c. Conversion to 3D: The new position is converted back into actual 3D world coordinates. Finally the object can be moved.

Vector3 screenPosition = new Vector3(
    Input.mousePosition.x,      // Mouse X
    Input.mousePosition.y,      // Mouse Y
    Camera.main.WorldToScreenPoint(_selectedGO.transform.position).z // Object's current depth                                   
);
// Convert back to world space
Vector3 worldPosition = Camera.main.ScreenToWorldPoint(screenPosition); 
_selectedGO.transform.position = worldPosition; // Move the object

Limitation: Keeping the object's original depth on the screen helps it stay in the same place compared to the camera as it moves with your mouse.

!!! This method works well, but it doesn't fit this game because players need to move the object along the ground, not through it.

Right now, the code keeps the object at a set distance from the camera, making it move on an unseen plane that's parallel to the camera. In this game, though, the objects should slide along the ground surface and stay in contact with it instead of floating at a fixed distance.

3.3.4 Final Solution: Plane-Constrained Dragging

The Breakthrough: Using Plane.Raycast for reliable, consistent movement

Unity has a Plane class that defines a virtual, infinite plane used for calculations, as it exists only in code. This virtual plane allows for consistent object height without needing a physical ground, keeping the height consistent while preventing objects from dropping or moving unexpectedly. It ensures smooth and precise movement and also consistent control of objects, independent of cursor speed.

Being virtual, the plane can be defined directly in code. Since the ground has a given position, we can save this position and use it to position or align the plane in calculations. The plane's normal (Vector3.up) helps us determine if we're looking at a flat surface. The ray direction is calculated from the camera to the mouse, and a hit occurs only if these directions intersect properly.

public class FurnitureDragger : MonoBehaviour 
{    
    [SerializeField] private Transform _plane;   // Reference to the plane transform
    [SerializeField] private LayerMask _furnitureMask = 1;

    private Plane _dragPlane;                    // Virtual plane's reference for dragging calculations
    private GameObject _selectedGO;              // Reference to the selected GameObject
    private bool _select = false;                // Boolean to track selection state    
    private Camera _camera;                      // Reference to the camera

    void Start()
    {
        _camera = Camera.main;

        // Null checks
        if (_camera == null || _plane == null)
        {
            Debug.LogError("Missing required references!");
            enabled = false;
            return;
        }

        // Initialize the infinite virtual plane at the position of _plane       
        _dragPlane = new Plane(Vector3.up, _plane.transform.position);
    }

    void Update()
    {
        HandleDragAndDrop();
    }

    private void HandleDragAndDrop()
    {
        // Select object under cursor
        Ray ray = _camera.ScreenPointToRay(Input.mousePosition);
        if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity, _furnitureMask))
        {
            _selectedGO = hit.collider.gameObject;
            _select = true;
            Cursor.visible = false; // Hide cursor during drag
        }

        if (Input.GetMouseButton(0) && _selectedGO != null)
        {
            Ray rayPlane = Camera.main.ScreenPointToRay(Input.mousePosition);

            // Check if the ray intersects with the virtual plane
            if (_dragPlane.Raycast(rayPlane, out float enter))
            {
                _selectedGO.transform.position = rayPlane.GetPoint(enter);
            }
        }

        if (Input.GetMouseButtonUp(0))
        {
            _select = false;
            _selectedGO = null;
            Cursor.visible = true;    // Clear selection
        }
    }
}

Note: The normal determines which side of the plane is detectable by the raycast.

Vector3.up makes the normal face up, so only rays coming from above will hit the plane. If a ray comes from below, it won't cross the plane. The plane's normal changes with rotation.

  • Rotation (0, 0, 0) – Horizontal Plane - Nomal: Vector3.up

      _dragPlane = new Plane(Vector3.up, _plane.transform.position);
    
  • Rotation (90, 0, 0) – Vertical Plane (Wall) - Nomal: Vector3.forward

      _dragPlane = new Plane(Vector3.forward, _plane.transform.position);
    

IV. Performance Analysis and Optimization

4.1 Optimization Results

  • Raycasting: I only check the furniture layer, which cuts down on extra calculations.

  • Material Handling: I keep renderer references handy to skip repeated GetComponent checks.

  • Update Frequency: I check for hover only when you’re not dragging anything.

4.2 Build Statitics and Metrics

  • Target FPS: 60fps (consistently achieved)

  • Build Size: Around 45MB

  • Memory Usage: Less than 200MB peak during gameplay


V. Visual Progress

Core Mechanics Demonstration

Moebel Finder - Drag-and-Drop GIF

Easy dragging of furniture with flat limit rule

Highlighting System

Moebel Finder - Highlighting System GIF

When you hover over it, the furniture turns red.

UI Integration

Moebel Finder - UI Integration GIF

Shows time and score when you match furniture.


VI. Challenges & Solutions Summary

6.1 Challenge 1: Drag Interruption

  • Problem: Quick mouse moves mess with drag tracking

  • Solution: A system where selections don't rely on hovering

  • Result: Drag works well no matter how fast the mouse is moved

6.2 Challenge 2: Inconsistent Object Depth

  • Problem: Items moved at any Z-depths without rules

  • Solution: System that keeps movement on a set plane

  • Result: Things stay where they should on tables or floors

6.3 Challenge 3: Performance with Multiple Objects

  • Problem: Non-stop raycasting for every item

  • Solution: Use of layers and chosen detection

  • Result: Smooth work even with more than 20 items to interact with


VII. Knowledge Synthesis and Lessons Learned

7.1 Technical Insights

  1. Plane.Raycast is good for flat areas and can be use this to note where you touch or click on large flat zones like ground or walls. It’s fast, good, quick, right on point and doesn't make the system slow.

  2. Tracking the Drag State is the Key for good drag-and-drop as it helps to watch closely if you are hovering, holding, or let go for a sharp drag-and-drop feel.

  3. Using a Layermask boost the performance as it helps by cutting down on the things Unity has to check, making it run better.

  4. Visual feedback (highlighting) makes it better for players. When items light up as you interact, it makes the feel of the game much better.

7.2 Development Process

  • Iterative problem-solving led to better answers than the first try.

  • Player feedback provided valuable information for improvements.

  • Early performance testing helped prevent problems that might have required optimization later


VIII. Next Steps : Improving the experience

Gameplay Polish

  • Set up a system to check drop zone.

  • Add some visual cues to show when placements are good.

  • Create a way for the game to get harder as players progress.

  • Adjust the difficulty based on what player’s feedback.

Progression & Replayability

  • Adjusting difficulty based on what players say

IX. Technical Specifications

9.1 Development Environment

  • Unity Version: 2022.3.62f1 LTS

  • Target Platform: Windows PC

  • Physics: Unity 3D Physics

9.2 Architecture

  • Design Pattern: Component-based architecture

  • Code Organization: Single-responsibility classes

  • Asset Management: Prefab-based furniture system


X. Accessibility and Community Engagement

10.1 Distribution Channels

Try It Yourself

10.2 Feedback Welcome

I'd love to hear your thoughts on the drag-and-drop mechanics! You can:

  • Comment below with your experience

  • Share suggestions for improvements

  • Report any bugs or issues encountered


XI. Development Stats

  • Total Development Time: 7 days

  • Lines of Code: ~500

  • Major Features: 3 core systems

  • Bugs Fixed: 12 during development

  • Performance Target: 60fps (achieved)

Tags

#gamedev #unity #indiedev #dragdrop #puzzle #timemanagement #c# #gameplay


Next week: Implementing advanced drop zone validation and visual feedback systems

0
Subscribe to my newsletter

Read articles from Yann K. directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Yann K.
Yann K.