Salahzar's Weblog

A collection of my posts in the web

Quattro script e un mondo…

… Roblox come interfaccia 3D per NPC intelligenti

Trentadue righe di Lua. Tanto basta per trasformare un mondo vuoto in una caccia al tesoro funzionante su Roblox. Un oggetto tocca il giocatore, incrementa un contatore, sparisce con un suono. Leaderboard a sinistra, barra progresso in alto, indicatore caldo/freddo che pulsa. Quattro script, nessun plugin, nessuna dipendenza esterna. Il codice completo è in appendice — scritto in collaborazione con Claude (Anthropic), perché la trasparenza su questi strumenti è il minimo sindacale.

Ma la cosa interessante non sono i quattro script. È quello che c’è sotto.

Chi non lo conosce dice che Roblox è un gioco per bambini. Chi ci mette le mani scopre un ambiente di sviluppo 3D con un linguaggio di scripting completo (Lua/Luau), un sistema di networking client-server integrato, e un HttpService che può chiamare qualsiasi API esterna — fino a 500 richieste al minuto per server. Se vuoi oggetti più complessi dei cubi primitivi, passi da Blender: modelli in FBX, riduzione vertici (il limite storico era 10.000 triangoli per mesh, oggi più rilassato ma l’ottimizzazione resta necessaria), UV mapping, texture. Il confronto più onesto non è con Minecraft. È con Unity — solo che la distribuzione è già risolta e il pubblico è già lì: 144 milioni di utenti attivi al giorno alla fine del 2025, 35 miliardi di ore di utilizzo annue.

E qui devo deviare, perché quel HttpService apre una porta che vale la pena esplorare.

Nel DevForum di Roblox ci sono già decine di progetti che collegano NPC a modelli linguistici esterni tramite chiamate HTTP. Il pattern è sempre lo stesso: il giocatore interagisce con un NPC, uno script server manda la richiesta a un’API esterna (OpenAI, Hugging Face, un server locale con Mistral o Llama), riceve la risposta in JSON, la mostra in una GUI o nella chat di gioco. C’è persino chi fa streaming audio con GPT-Realtime per NPC a controllo vocale. Il codice base è quasi banale:

local HttpService = game:GetService("HttpService")
-- Manda il messaggio del giocatore a un server esterno
-- e riceve la risposta dell'NPC in formato JSON
local response = HttpService:PostAsync(
"https://tuo-server.com/api/chat",
HttpService:JSONEncode({message = playerMessage, context = npcContext}),
Enum.HttpContentType.ApplicationJson
)
local data = HttpService:JSONDecode(response)

Chi ha familiarità con il progetto Nexus — il sistema di NPC intelligenti per OpenSimulator che integra modelli linguistici con ambienti virtuali 3D — riconosce immediatamente l’architettura. In OpenSimulator lo script LSL chiamava un endpoint HTTP esterno, mandava il contesto della conversazione, riceveva la risposta dal modello e la mostrava in chat. In Roblox si fa la stessa cosa con Luau e HttpService. Cambia la sintassi, non il disegno.

La differenza pratica, però, è enorme. OpenSimulator richiedeva installazione server, configurazione di rete, client dedicato, e raggiungeva qualche centinaio di utenti. Roblox è un’applicazione scaricata su centinaia di milioni di dispositivi. Pubblicare un’esperienza con NPC intelligenti su Roblox significa renderla accessibile a chiunque abbia un telefono. Non è un dettaglio.

Vabbè, ma funziona davvero? Dipende da cosa intendi per “funziona”.

Il primo muro è il limite dei 500 richieste/minuto per server. In un’esperienza con 30 giocatori che parlano contemporaneamente con NPC, si arriva rapidamente al tetto. In Unity o Unreal, con SDK nativi e connessioni persistenti, questo collo di bottiglia non esiste: gestisci il traffico come vuoi. In Roblox tutto passa per un unico tubo HTTP, e il tubo ha un diametro fisso. La latenza della risposta del modello si somma a quella della rete. Per un esperimento va bene. Per un prodotto scalabile, è il confine.

Il secondo muro è più sottile e più insidioso: la coerenza tra il cervello esterno e il mondo 3D. Se l’NPC dice “apri il portone” e il portone è già stato distrutto da un altro giocatore, l’immersione crolla. Il modello linguistico non vede il DataModel di Roblox — vede solo il contesto che gli mandi nel prompt. Quindi prima di ogni PostAsync devi raccogliere lo stato corrente degli oggetti rilevanti e serializzarlo. Un ponte di memoria, in pratica: un database intermedio (Redis, SQLite, anche un semplice dizionario Lua persistito via DataStoreService) che tiene traccia delle mutazioni del mondo e le inietta nel contesto della conversazione. È esattamente il problema che Nexus affronta su OpenSimulator, con la complicazione che in Roblox il mondo è più dinamico e i giocatori sono molti di più. Il costo in token cresce, la finestra di contesto si riempie, e a un certo punto devi decidere cosa il tuo NPC “dimentica”. Progettare l’oblio è più difficile che progettare la memoria.

Il terzo muro è la moderazione. Roblox ha filtri rigidi sulla chat — una scatola nera che può troncare o censurare risposte generate dal modello esterno perché contengono parole che il sistema considera problematiche. Per un gioco qualsiasi, è un fastidio. Per un NPC educativo che deve parlare di storia, anatomia, conflitti sociali, è un limite strutturale. Prova a far dire al tuo NPC una frase sulla Shoah o sull’apparato riproduttivo: il filtro potrebbe intervenire prima che il contenuto arrivi al giocatore. Non per malafede — per eccesso di prudenza automatizzata. Chi progetta NPC didattici su Roblox si ritrova a negoziare con un censore che non distingue contesto educativo da contenuto inappropriato.

E qui si apre un quarto problema che nessuno dei tutorial sul DevForum menziona: i dati. Se il giocatore ha tredici anni e manda un messaggio a un NPC, e quello script PostAsync gira il messaggio a un’API OpenAI o a un server Hugging Face, i dati di un minore escono dalla piattaforma e finiscono su infrastrutture di terzi. Roblox non lo vieta esplicitamente — per ora. Ma il GDPR è piuttosto chiaro sul trattamento dei dati dei minori, e questa è un’area grigia che potrebbe chiudersi in qualsiasi momento. Chi costruisce esperienze educative con NPC intelligenti su Roblox dovrebbe almeno porsi la domanda. E documentare la risposta.

Il programma educativo ufficiale di Roblox esiste dal 2018 — Learning Hub, partnership con BBC Bitesize, Project Lead the Way, Google. Non c’è nessuno scandalo in questo, e non c’è nessuna rivelazione: è lo stesso schema visto con Minecraft Education, con OpenSimulator prima ancora, con Second Life ai tempi del MIUR. Piattaforma 3D + contenuti didattici + entusiasmo istituzionale + implementazione lasciata ai singoli docenti. Il ciclo si ripete con la puntualità di un orologio svizzero.

Fellini in Amarcord filma un paese intero che guarda passare il transatlantico Rex nella nebbia — tutti fermi ad aspettare qualcosa di enorme che in realtà è solo una sagoma. Le piattaforme educative funzionano così: il transatlantico è la tecnologia, il paese è la scuola, la nebbia è il divario tra promessa e pratica. Cambia solo la sagoma. La questione non è se Roblox sia educativo — qualsiasi ambiente programmabile lo è, se qualcuno progetta l’esperienza con criterio. E finché la scuola italiana vedrà Roblox come un libro di testo 3D invece che come un laboratorio di logica computazionale — dove il Luau è linguistica applicata e il prompt è retorica — resterà sulla spiaggia a guardare la sagoma. I dati GoStudent 2025 dicono che il 13% delle scuole integra strumenti AI nei programmi didattici. Per Roblox le cifre italiane non esistono nemmeno.

Ma — e qui torno al punto — la cosa interessante di Roblox nel 2025 non è l’ennesima promessa educativa. È che si tratta di un ambiente 3D con scripting, networking e chiamate HTTP esterne, distribuito su 144 milioni di dispositivi, con una pipeline di importazione asset da Blender che funziona. Per chi lavora su NPC intelligenti in mondi virtuali, è un front-end già pronto — con tutti i limiti di un front-end che non controlli fino in fondo. Lo script che in OpenSimulator chiamava il server Nexus in LSL si riscrive in Luau in mezz’ora. Il mondo 3D che in OpenSimulator costruivi con i prim lo costruisci con MeshPart importati da Blender. La chat che in OpenSimulator passava per llSay() passa per RemoteEvent e una TextLabel. Poi ti scontri con la moderazione, i limiti HTTP, la coerenza di stato, il GDPR. Ma almeno sai dove sono i muri.

Resta il problema di sempre: il codice è la parte facile. Chi decide cosa fa dire l’NPC? Quale contesto gli dai? Come gestisci la memoria della conversazione — e soprattutto, come gestisci l’oblio? Come eviti che generi risposte incoerenti con l’ambientazione, con la storia del giocatore, con lo stato del mondo che cambia mentre parlano? Questi sono problemi di progettazione, non di programmazione — e sono gli stessi problemi che Nexus affronta in OpenSimulator. Cambio piattaforma, stesso mestiere.

Quattro script per la caccia al tesoro. Un PostAsync per collegare un cervello esterno. Un ponte di memoria per non perdere il filo. Il mondo lo devi riempire tu — ma adesso almeno sai quanta gente potrebbe entrarci, e cosa rischi quando lo fai.


Appendice: il codice

Scritto in collaborazione con un modello AI (Claude, Anthropic). La struttura degli script è generata dal modello; la revisione, il testing e l’adattamento al contesto sono umani. Il codice è libero — copiate, modificate, migliorate.

Setup: nella Workspace di Roblox Studio, creare una Folder chiamata Collectibles. Mettere dentro i Part con nome nel formato Part.5, Part.10 (il numero dopo il punto è il valore in oro). Ogni Part contiene una copia dello Script 2.

Script 1 — GameManager (ServerScriptService)

Gestisce leaderboard, conteggio oggetti e condizione di vittoria.

local Players = game.Players
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Conta gli oggetti nella cartella Collectibles all'avvio del server.
-- Questo numero diventa il totale da trovare per la vittoria.
local collectiblesFolder = workspace:WaitForChild("Collectibles")
local totalItems = #collectiblesFolder:GetChildren()
-- RemoteEvent: canale di comunicazione server → client.
-- Il server manda aggiornamenti, il client li riceve e aggiorna la GUI.
local progressEvent = Instance.new("RemoteEvent")
progressEvent.Name = "UpdateProgress"
progressEvent.Parent = ReplicatedStorage
local victoryEvent = Instance.new("RemoteEvent")
victoryEvent.Name = "Victory"
victoryEvent.Parent = ReplicatedStorage
-- Ogni volta che un giocatore entra nel server:
Players.PlayerAdded:Connect(function(player)
-- leaderstats: cartella speciale che Roblox mostra automaticamente
-- nella leaderboard laterale. Il nome DEVE essere "leaderstats" minuscolo.
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
-- Gold: valore visibile in leaderboard, si incrementa al tocco
local gold = Instance.new("IntValue")
gold.Name = "Gold"
gold.Value = 0
gold.Parent = leaderstats
-- Found: contatore oggetti trovati, serve per il check di vittoria
local found = Instance.new("IntValue")
found.Name = "Found"
found.Value = 0
found.Parent = leaderstats
-- Ogni volta che Found cambia, notifica il client.
-- Se Found == totalItems, il giocatore ha vinto.
found.Changed:Connect(function(newValue)
progressEvent:FireClient(player, newValue, totalItems)
if newValue >= totalItems then
victoryEvent:FireClient(player)
end
end)
-- Manda il conteggio iniziale (0/totale) dopo un breve ritardo
-- per dare tempo al client di caricare la GUI
task.wait(1)
progressEvent:FireClient(player, 0, totalItems)
end)

Script 2 — Part Handler (Script dentro ogni Part in Collectibles)

Gestisce il tocco, il punteggio e la distruzione dell’oggetto.

local object = script.Parent
local isTouched = false -- flag anti-doppio tocco
-- Ricava il valore in oro dal nome dell'oggetto.
-- "Part.5" → split(".") → {"Part", "5"} → tonumber("5") → 5
-- Se il nome non ha il punto (es. "Part"), goldAmount = 1
local nameSplit = string.split(object.Name, ".")
local goldAmount = tonumber(nameSplit[2]) or 1
-- Touched si attiva quando qualsiasi cosa collide con il Part:
-- altri Part, il terreno, il personaggio del giocatore.
-- Bisogna filtrare: ci interessa solo il tocco di un giocatore.
object.Touched:Connect(function(hit)
if isTouched then return end -- già raccolto, ignora
-- hit è la parte che ha toccato (es. una gamba del personaggio).
-- hit.Parent è il Model del personaggio (contiene Humanoid, Head, ecc.)
local character = hit.Parent
local humanoid = character:FindFirstChild("Humanoid")
if humanoid then
-- Risali dal personaggio al Player (oggetto server)
local player = game.Players:GetPlayerFromCharacter(character)
if player then
isTouched = true -- blocca tocchi multipli
-- Aggiorna i contatori nella leaderboard del giocatore
player.leaderstats.Gold.Value += goldAmount
player.leaderstats.Found.Value += 1
-- Suono di raccolta: feedback immediato.
-- rbxassetid://6895079853 è un suono generico "coin".
-- Sostituire con un ID dalla libreria Roblox se preferito.
local sound = Instance.new("Sound")
sound.SoundId = "rbxassetid://6895079853"
sound.Parent = object
sound:Play()
-- Sparisce subito (trasparenza), poi si autodistrugge
-- dopo mezzo secondo per lasciar finire il suono
object.Transparency = 1
task.wait(0.5)
object:Destroy()
end
end
end)

Script 3 — Barra Progresso (LocalScript in StarterPlayerScripts)

GUI con contatore “Trovati: X / Y” e barra animata.

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local player = Players.LocalPlayer
-- Crea la GUI a schermo. ScreenGui è il contenitore,
-- tutto quello dentro appare sovrapposto al mondo 3D.
local screenGui = Instance.new("ScreenGui")
screenGui.Name = "ProgressGui"
screenGui.Parent = player:WaitForChild("PlayerGui")
-- Frame: rettangolo semi-trasparente centrato in alto.
-- UDim2 usa due valori per asse: (scala 0-1, offset in pixel).
-- Position (0.5, -110, 0, 10) = centrato orizzontalmente, 10px dal bordo superiore.
local frame = Instance.new("Frame")
frame.Size = UDim2.new(0, 220, 0, 50)
frame.Position = UDim2.new(0.5, -110, 0, 10)
frame.BackgroundColor3 = Color3.fromRGB(30, 30, 30)
frame.BackgroundTransparency = 0.3
frame.Parent = screenGui
Instance.new("UICorner", frame).CornerRadius = UDim.new(0, 8) -- angoli arrotondati
-- Testo "Trovati: 0 / ?" in giallo oro
local label = Instance.new("TextLabel")
label.Size = UDim2.new(1, 0, 1, 0) -- riempie tutto il frame
label.BackgroundTransparency = 1 -- sfondo invisibile
label.TextColor3 = Color3.fromRGB(255, 215, 0)
label.TextScaled = true
label.Font = Enum.Font.GothamBold
label.Text = "Trovati: 0 / ?"
label.Parent = frame
-- Barra di sfondo (grigio scuro) posizionata in basso nel frame
local bar = Instance.new("Frame")
bar.Size = UDim2.new(0, 200, 0, 6)
bar.Position = UDim2.new(0.5, -100, 1, -12)
bar.BackgroundColor3 = Color3.fromRGB(60, 60, 60)
bar.Parent = frame
Instance.new("UICorner", bar).CornerRadius = UDim.new(0, 3)
-- Barra di riempimento (oro) — parte da larghezza 0, cresce con TweenSize
local fill = Instance.new("Frame")
fill.Size = UDim2.new(0, 0, 1, 0) -- larghezza 0 all'inizio
fill.BackgroundColor3 = Color3.fromRGB(255, 215, 0)
fill.Parent = bar
Instance.new("UICorner", fill).CornerRadius = UDim.new(0, 3)
-- Ascolta l'evento dal server: riceve (trovati, totale)
ReplicatedStorage:WaitForChild("UpdateProgress").OnClientEvent:Connect(
function(found, total)
label.Text = "Trovati: " .. found .. " / " .. total
-- Calcola il rapporto e anima la barra con TweenSize.
-- EasingStyle.Quad + EasingDirection.Out = decelerazione morbida.
local ratio = total > 0 and (found / total) or 0
fill:TweenSize(
UDim2.new(ratio, 0, 1, 0),
Enum.EasingDirection.Out,
Enum.EasingStyle.Quad, 0.3, true
)
end
)
-- Vittoria: cambia colore e testo
ReplicatedStorage:WaitForChild("Victory").OnClientEvent:Connect(function()
label.Text = "TUTTI TROVATI!"
label.TextColor3 = Color3.fromRGB(0, 255, 100)
fill.BackgroundColor3 = Color3.fromRGB(0, 255, 100)
end)

Script 4 — Indicatore Caldo/Freddo (LocalScript in StarterPlayerScripts)

Prossimità al collezionabile più vicino. Opzionale ma rende l’esplorazione giocabile.

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local player = Players.LocalPlayer
local screenGui = Instance.new("ScreenGui")
screenGui.Name = "HintGui"
screenGui.Parent = player:WaitForChild("PlayerGui")
local hint = Instance.new("TextLabel")
hint.Size = UDim2.new(0, 160, 0, 30)
hint.Position = UDim2.new(0.5, -80, 0, 65)
hint.BackgroundTransparency = 1
hint.TextScaled = true
hint.Font = Enum.Font.Gotham
hint.Text = ""
hint.Parent = screenGui
local collectiblesFolder = workspace:WaitForChild("Collectibles")
-- Ricava la posizione sia da Part che da Model
local function getPosition(obj)
if obj:IsA("BasePart") then
return obj.Position
elseif obj:IsA("Model") then
-- PrimaryPart se impostato, altrimenti primo BasePart trovato
local primary = obj.PrimaryPart
or obj:FindFirstChildWhichIsA("BasePart", true)
if primary then
return primary.Position
end
end
return nil
end
RunService.Heartbeat:Connect(function()
local character = player.Character
if not character then return end
local root = character:FindFirstChild("HumanoidRootPart")
if not root then return end
local nearest = math.huge
for _, obj in collectiblesFolder:GetChildren() do
local pos = getPosition(obj)
if pos then
local dist = (pos - root.Position).Magnitude
if dist < nearest then nearest = dist end
end
end
if nearest < 15 then
hint.Text = "Vicinissimo!"
hint.TextColor3 = Color3.fromRGB(255, 80, 80)
elseif nearest < 40 then
hint.Text = "Caldo..."
hint.TextColor3 = Color3.fromRGB(255, 180, 50)
elseif nearest < 80 then
hint.Text = "Freddo"
hint.TextColor3 = Color3.fromRGB(100, 150, 255)
else
hint.Text = ""
end
end)

Fonti

[1] Roblox Corporation, risultati Q4 2025 — 144M DAU, 35 mld ore, $1,4 mld ricavi (multiplayer.it, febbraio 2026)

[2] Roblox HttpService API Reference — 500 req/min, supporto GET/POST/JSON, SSE in Studio (create.roblox.com/docs)

[3] DevForum Roblox, “Creating a ChatGPT-Integrated NPC” — pattern proxy server + HttpService (giugno 2024)

[4] DevForum Roblox, “ezLLM: Integrating an AI System into Roblox” — Claude, GPT-4 Mini, Llama via DuckDuckGo Chat (gennaio 2025)

[5] Roblox Education e Learning Hub — programma attivo dal 2018, partnership BBC/Google/PLTW (ir.roblox.com, 2025)

[6] GoStudent, “Report sul futuro dell’istruzione 2025” — 13% scuole italiane integra AI nei programmi didattici

[7] Grow a Garden: 21,3M giocatori simultanei, creatore sedicenne, $12M ricavi a maggio 2025 (hdblog.it, luglio 2025)

[8] DevForum Roblox, “How to get past the 10,000 triangle limit” — pipeline Blender→FBX→Roblox con riduzione mesh (settembre 2022)

[9] Regolamento UE 2016/679 (GDPR), art. 8 — Condizioni applicabili al consenso dei minori in relazione ai servizi della società dell’informazione

Leave a comment