Roblox · Дополнительное

NPC с диалогом

Житель: подходишь, E «Поговорить» — внизу окно с его именем, реплика печатается по буквам, под ней варианты ответа. Ответ ведёт к следующей реплике или заканчивает разговор. Все диалоги — в одной таблице.

Структура

Синим — что нужно создать.

Workspace
└─ Villager (Model) — NPC: HumanoidRootPart Anchored
   └─ HumanoidRootPart → TalkPrompt (ProximityPrompt) — имя важно
ReplicatedStorage
└─ Dialogs (ModuleScript) — реплики всех NPC
StarterGui
└─ DialogGui (ScreenGui) — ResetOnSpawn выкл.
   └─ Frame (Frame) — Visible выкл.: NameLabel, TextLabel, Options (UIListLayout), Close
StarterPlayer → StarterPlayerScripts
└─ DialogLocal (LocalScript)

Шаги

  1. NPC: Avatar → Rig Builder, имя модели — как ключ в Dialogs (Villager), HumanoidRootPart Anchored (стоит на месте). В HumanoidRootPart — ProximityPrompt с именем «TalkPrompt».
  2. ReplicatedStorage → ModuleScript «Dialogs»: имя NPC → реплики. У реплики Text и Options; у варианта Text и Next (имя следующей реплики; без Next — конец). Первая реплика — всегда start.
  3. StarterGui → ScreenGui «DialogGui» → Frame внизу (Visible выкл.): TextLabel «NameLabel», «TextLabel» (TextWrapped), Frame «Options» с UIListLayout, TextButton «Close». Кнопки ответов скрипт создаёт сам.
  4. Печать по буквам — свойство MaxVisibleGraphemes: русские буквы не ломаются (с string.sub они бы рассыпались).
  5. Диалог работает на клиенте: сервер не нужен. Новый NPC — модель с TalkPrompt и раздел в Dialogs.

Скрипты

Dialogs

Где: ReplicatedStorage · ModuleScript
Зачем: Реплики и варианты ответов.

-- все диалоги: имя NPC → его реплики
local dialogs = {
    -- имя — как у модели NPC
    ["Villager"] = {
        -- первая реплика всегда start
        start = {
            -- что говорит NPC
            Text = "Привет, путник! Ты здесь впервые?",
            -- варианты ответа
            Options = {
                -- Next — к какой реплике перейти
                {Text = "Да, что тут у вас?", Next = "about"},
                -- без Next — разговор окончен
                {Text = "Нет, пока!"},
            -- конец вариантов
            },
        -- конец реплики
        },
        -- реплика about
        about = {
            -- текст
            Text = "Наша деревня. За холмом живут монстры — будь осторожен.",
            -- варианты
            Options = {
                -- дальше
                {Text = "А как их победить?", Next = "tip"},
                -- конец
                {Text = "Спасибо!"},
            -- конец вариантов
            },
        -- конец реплики
        },
        -- реплика tip
        tip = {
            -- текст
            Text = "Возьми меч в магазине и прячься за укрытиями.",
            -- один вариант
            Options = {{Text = "Понял, спасибо!"}},
        -- конец реплики
        },
    -- конец диалога
    },
-- конец таблицы
}

-- отдаём таблицу
return dialogs

DialogLocal

Где: StarterPlayer → StarterPlayerScripts · LocalScript
Зачем: Окно диалога и переходы.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- все надписи ProximityPrompt
local PPS = game:GetService("ProximityPromptService")
-- реплики всех NPC
local dialogs = require(RS:WaitForChild("Dialogs"))
-- этот игрок
local player = game.Players.LocalPlayer
-- окно диалога
local gui = player:WaitForChild("PlayerGui"):WaitForChild("DialogGui")
-- окно
local frame = gui:WaitForChild("Frame")
-- имя NPC
local nameLabel = frame:WaitForChild("NameLabel")
-- реплика
local textLabel = frame:WaitForChild("TextLabel")
-- кнопки ответов
local options = frame:WaitForChild("Options")
-- с каким диалогом говорим
local talking = nil
-- номер реплики — чтобы старая печать не мешала новой
local version = 0

-- закончить разговор
local function Close()
    -- ни с кем
    talking = nil
    -- окно прячем
    frame.Visible = false
-- конец функции
end

-- показать реплику
local function ShowNode(nodeName)
    -- реплика из таблицы
    local node = talking and talking[nodeName]
    -- нет такой — конец
    if not node then return Close() end
    -- новая реплика
    version += 1
    -- запоминаем свою
    local my = version
    -- старые кнопки
    for _, child in ipairs(options:GetChildren()) do
        -- убираем
        if child:IsA("TextButton") then child:Destroy() end
    -- конец перебора
    end
    -- весь текст
    textLabel.Text = node.Text
    -- но пока не видно ни буквы
    textLabel.MaxVisibleGraphemes = 0
    -- по одной букве (русские буквы считаем правильно)
    for i = 1, utf8.len(node.Text) do
        -- уже показали другую реплику
        if my ~= version then return end
        -- показываем i букв — эффект печати
        textLabel.MaxVisibleGraphemes = i
        -- скорость печати
        task.wait(0.02)
    -- конец цикла
    end
    -- весь текст
    textLabel.MaxVisibleGraphemes = -1
    -- каждый вариант ответа
    for i, option in ipairs(node.Options or {}) do
        -- кнопка
        local button = Instance.new("TextButton")
        -- во всю ширину
        button.Size = UDim2.new(1, 0, 0, 34)
        -- фон
        button.BackgroundColor3 = Color3.fromRGB(60, 60, 80)
        -- белый текст
        button.TextColor3 = Color3.new(1, 1, 1)
        -- по размеру
        button.TextScaled = true
        -- жирный
        button.Font = Enum.Font.GothamBold
        -- текст ответа
        button.Text = option.Text
        -- порядок
        button.LayoutOrder = i
        -- в список
        button.Parent = options
        -- выбрали ответ
        button.MouseButton1Click:Connect(function()
            -- дальше или конец
            if option.Next then ShowNode(option.Next) else Close() end
        -- конец обработчика
        end)
    -- конец перебора
    end
    -- вариантов нет
    if not node.Options or #node.Options == 0 then
        -- пусть дочитает
        task.wait(2)
        -- и закрываем
        if my == version then Close() end
    -- конец проверки
    end
-- конец функции
end

-- нажал E у надписи
PPS.PromptTriggered:Connect(function(prompt)
    -- это не разговор
    if prompt.Name ~= "TalkPrompt" then return end
    -- NPC, к которому подошли
    local npc = prompt:FindFirstAncestorOfClass("Model")
    -- для него нет диалога
    if not npc or not dialogs[npc.Name] then return end
    -- его реплики
    talking = dialogs[npc.Name]
    -- имя
    nameLabel.Text = npc.Name
    -- открываем окно
    frame.Visible = true
    -- первая реплика
    ShowNode("start")
-- конец обработчика
end)
-- крестик — закончить разговор
frame:WaitForChild("Close").MouseButton1Click:Connect(Close)

Проверка

Запусти Play (или Run) и убедись:

Собрать NPC с диалогом одной командой