← Back to Portal Hub

로블록스 커스텀 핫바 아키텍처: 드래그 앤 드롭 고스트, 포인터 이벤트 및 모바일 터치 UX

By DopaBrain UI/UX 시스템 팀 • 2026-09-30 • Technical Guide

로블록스의 서바이벌 샌드박스, RPG, 배틀로얄 게임에서 인벤토리와 핫바는 플레이어가 가장 빈번하게 조작하는 핵심 인터페이스입니다. 로블록스가 기본 제공하는 백팩(Backpack)은 디자인이 구식이고, 아이템 나누기나 슬롯 정렬을 지원하지 않으며, 모바일 환경에서 오작동이 자주 발생합니다. 이를 대체하는 커스텀 드래그 앤 드롭 시스템은 상용 게임 제작의 필수 요건입니다.

본 시스템 엔지니어링 가이드에서는 크로스 플랫폼 인벤토리 컨트롤러를 설계합니다. InputObject 포인터 이벤트 수신, 드래그 도중 마우스를 따라다니는 고스트 아이콘 연출, 슬롯 충돌 판정, 서버 권한 슬롯 교체 검증, 키보드 숫자키(1~9) 및 모바일 터치 제스처 연동을 상세히 다룹니다.

1. 드래그 앤 드롭의 메커니즘: 포인터 이벤트와 드래그 임계값

신뢰할 수 있는 드래그 앤 드롭 UI를 구축하려면 단순 클릭과 의도적인 드래그를 명확히 구분해야 합니다:

2. 상용급 클라이언트 드래그 앤 드롭 컨트롤러 구현 코드

아래는 부드러운 고스트 연출, 슬롯 충돌 판정, 서버 슬롯 스왑 요청을 구현한 클라이언트 측 Luau 모듈입니다:

InventoryDragDropController.luau (클라이언트 인벤토리 컨트롤러)
--!strict
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Players = game:GetService("Players")

local DragController = {}
DragController.__index = DragController

local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui") :: PlayerGui

export type SlotData = {
    Frame: Frame,
    SlotIndex: number,
    ItemId: string?,
    Count: number
}

local registeredSlots: { [Frame]: SlotData } = {}
local activeDrag: {
    SourceSlot: SlotData,
    Ghost: ImageLabel,
    StartPos: Vector2,
    IsDragging: boolean
}? = nil

function DragController.RegisterSlot(frame: Frame, slotIndex: number, itemId: string?, count: number)
    registeredSlots[frame] = {
        Frame = frame,
        SlotIndex = slotIndex,
        ItemId = itemId,
        Count = count
    }

    frame.InputBegan:Connect(function(input: InputObject)
        if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
            DragController.StartDrag(frame, input.Position)
        end
    end)
end

function DragController.StartDrag(slotFrame: Frame, inputPos: Vector3)
    local data = registeredSlots[slotFrame]
    if not data or not data.ItemId then return end

    local icon = slotFrame:FindFirstChild("ItemIcon") :: ImageLabel?
    if not icon then return end

    local ghost = icon:Clone()
    ghost.Size = UDim2.new(0, icon.AbsoluteSize.X * 1.1, 0, icon.AbsoluteSize.Y * 1.1)
    ghost.Position = UDim2.new(0, inputPos.X, 0, inputPos.Y)
    ghost.AnchorPoint = Vector2.new(0.5, 0.5)
    ghost.ZIndex = 100
    ghost.Active = false
    ghost.Parent = playerGui:FindFirstChildOfClass("ScreenGui")

    activeDrag = {
        SourceSlot = data,
        Ghost = ghost,
        StartPos = Vector2.new(inputPos.X, inputPos.Y),
        IsDragging = true
    }
end

UserInputService.InputChanged:Connect(function(input: InputObject)
    if activeDrag and activeDrag.IsDragging then
        if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch then
            activeDrag.Ghost.Position = UDim2.new(0, input.Position.X, 0, input.Position.Y)
        end
    end
end)

UserInputService.InputEnded:Connect(function(input: InputObject)
    if not activeDrag or not activeDrag.IsDragging then return end
    if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
        local dropPos = Vector2.new(input.Position.X, input.Position.Y)
        local targetSlot: SlotData? = nil

        for frame, slotData in pairs(registeredSlots) do
            local pos = frame.AbsolutePosition
            local size = frame.AbsoluteSize
            if dropPos.X >= pos.X and dropPos.X <= (pos.X + size.X) and
               dropPos.Y >= pos.Y and dropPos.Y <= (pos.Y + size.Y) then
                targetSlot = slotData
                break
            end
        end

        if targetSlot and targetSlot ~= activeDrag.SourceSlot then
            -- 서버로 슬롯 스왑 RemoteEvent 호출
            -- InventoryRemote:FireServer("SwapSlots", activeDrag.SourceSlot.SlotIndex, targetSlot.SlotIndex)
        end

        activeDrag.Ghost:Destroy()
        activeDrag = nil
    end
end)

return DragController

3. 핫바 숫자키(1~9) 바인딩 및 인체공학

핫바는 플레이어의 손발과 같습니다. 긴박한 전투에서 마우스 조작 없이 즉각 원하는 도구를 꺼내는 단축키 체계가 필수적입니다:

4. 모바일 크로스플랫폼 터치 제스처 및 안전 영역

스마트폰은 마우스 우클릭이 없습니다. 모바일 환경에 특화된 직관적인 터치 제스처를 설계해야 합니다:

5. 상용 배포 검증 및 네트워크 동기화 체크리스트

수천 명의 동시 접속자가 플레이하는 라이브 환경에서 무결성을 유지하기 위한 QA 체크리스트입니다:

Frequently Asked Questions

로블록스 기본 백팩(Backpack)을 비활성화해야 하는 이유는 무엇인가요?

기본 백팩은 디자인 커스터마이징이 불가능하고, 아이템 나누기나 슬롯 정렬을 지원하지 않으며 모바일 터치 오류가 잦습니다. SetCoreGuiEnabled(Enum.CoreGuiType.Backpack, false)로 비활성화하고 전용 GUI를 만드는 것이 권장됩니다.

드래그 중인 아이템이 마우스 커서를 부드럽게 따라오게 하려면 어떻게 하나요?

화면 최상위 ScreenGui에 아이콘을 복제한 후 AnchorPoint를 (0.5, 0.5)로 설정하고, InputChanged 이벤트에서 input.Position 좌표로 고스트 아이콘의 Position을 실시간 갱신해주면 됩니다.

빠르게 드래그할 때 아이템이 복사되는 버그를 어떻게 막나요?

모든 슬롯 교체 연산은 반드시 서버에서 승인되어야 합니다. 서버는 원래 슬롯에 해당 아이템이 실제로 존재하는지 확인하고, 대상 슬롯과 인덱스를 맞바꾼 후 클라이언트로 최종 결과를 동기화합니다.

모바일에서 우클릭 없이 아이템을 어떻게 반으로 나누나요?

400ms(0.4초) 이상 슬롯을 누르고 있으면 나타나는 롱프레스 팝업 슬라이더를 구현하여 모바일 플레이어도 손쉽게 수량을 분할할 수 있도록 합니다.

Explore More Interactive Tests & Guides

Discover personalized cognitive assessments, stress evaluations, and game psychology tools on DopaBrain.

Go to Portal Hub