Games / Roblox
Who it's for. You build a Roblox place the NPC should play (obby, paths, aiming, or your own).
What you'll do. Name parts, kill on lava, publish JumpNow on movers, then ship PlayBatch/PlayRuntime — never policy_weights.
Download the live pack: SDK · API. The NPC does not read your game's internal data. It sees only what its rays hit — classified by part names — then the Hub returns move / jump / fire. Web companion · Licensing · Deploy & compete
Making your game AI-ready means recognized naming, walkable physics, jumpable scale, a reachable goal, reliable death detection, and a spawn. Live compete places ship PlayRuntime + VisionSystem only — never policy_weights. Campus Train my AI player is the training exception.
The classifier reads part names. Without recognized prefixes, everything is an unknown obstacle.
| Role | Name pattern | Examples |
|---|---|---|
| Platform | Platform_*, Floor*, Spawn | Platform_1, Floor_Main, Spawn |
| Hazard | Hazard_*, Lava* | Hazard_Lava_1, LavaFloor |
| Reward | Coin_*, Reward_* | Coin_1, Reward_Gem |
| Goal | FinishLine, Finish, Exit | FinishLine, Exit |
| Wall | Wall_* | Wall_North, Wall_1 |
| Target | Target_* | Target_1, Target_Boss |
| Track / ramp | Track_*, Ramp_* | Classified as platform (truck / ramps) |
| Smash / slow / bounce | Smash_*, Slow_*, Bounce_* | Classified as hazard |
| Mover | Platform_* + KitMove (preferred) | MovingPad_1 is not a vision prefix — use JumpNow overlay |
Color fallback if the name is unknown: bright red → hazard, yellow → reward. Vision uses RespectCanCollide = false so lava is still seen. Coins: CanCollide = false, CanQuery = true. HRP does not collide; limbs do.
| Object | CanCollide | Why |
|---|---|---|
| Platforms | true | NPC must stand on them |
| Hazards (lava, pits) | false | NPC falls through; Touched kills it |
| Rewards (coins) | false | NPC walks through to collect |
| Walls | true | NPC must not pass through |
If lava is solid, the NPC stands on it instead of dying. Always CanCollide = false plus a kill handler.
-- ServerScriptService: name platforms and set collision
local function tagPlatform(part, index)
part.Name = "Platform_" .. index
part.CanCollide = true
part.Anchored = true
end
local function tagWall(part, name)
part.Name = "Wall_" .. name
part.CanCollide = true
part.Anchored = true
end
local function tagCoin(part, index)
part.Name = "Coin_" .. index
part.CanCollide = false
part.CanQuery = true -- still visible to vision rays
end| Property | Limit | Rationale |
|---|---|---|
| Max gap distance | 12 studs (hard ceiling) | JumpPower 70 ceiling — do not train as default |
| Recommended learn gap | 2–4 studs (advanced ≤ 8) | Training gyms in this project |
| Max height difference | 5 studs | ~5 stud jump-up reliably |
| Min platform size | 3×3 studs | Smaller pads are hard to land on |
| WalkSpeed | 16–24 | Panic sprint is animation, not a new walk speed |
| Use | Gap | Height Δ | JumpPower | WalkSpeed |
|---|---|---|---|---|
| Training gyms | 2–4 studs | 0–3 | ~50 | 16–24 |
| Hard ceiling | ~8 studs | ~5 with boost | higher | — |
| Do not promise | 12 as default Humanoid | — | JumpPower 70 as default | — |
Maze corridors ≥ 4 studs. Surge-style lanes: player radius 0.7, clearance ≥ 0.5, no two adjacent bumper lanes.
The goal must be reachable from spawn via safe zones. Place yourself at spawn and try to finish. If you cannot, the AI cannot either.
local function killOnTouch(lavaPart)
lavaPart.Name = "Hazard_Lava_1"
lavaPart.CanCollide = false
lavaPart.Size = Vector3.new(lavaPart.Size.X, 1, lavaPart.Size.Z) -- 1 stud tall
lavaPart.Touched:Connect(function(hit)
local char = hit:FindFirstAncestorWhichIsA("Model")
if not char then
return
end
local humanoid = char:FindFirstChildOfClass("Humanoid")
if humanoid then
humanoid.Health = 0
end
end)
end-- In your 0.1s tick, after encoding obs — do not wait on HTTP
if hrp.Position.Y < 2.5 and isInHazardBounds(hrp.Position) then
humanoid.Health = 0
endCommon pitfalls:
-- One-time world cleanup for a gym (server)
if workspace:FindFirstChild("Baseplate") then
workspace.Baseplate:Destroy()
end
workspace.Terrain:Clear()local spawnPart = workspace:FindFirstChild("Spawn") or workspace:FindFirstChildWhichIsA("SpawnLocation")
assert(spawnPart, "Name a Spawn part or add a SpawnLocation")
npc:PivotTo(CFrame.new(spawnPart.Position + Vector3.new(0, 3, 0)))Tune FOV for mazes, range for open worlds, edge probes for precision platforming.
-- TrainConfig.lua (campus) or a ModuleScript next to PlayRuntime (live)
local TrainConfig = {}
TrainConfig.VISION = {
range = 30, -- max ray distance in studs
origin_offset_y = 1.5, -- eye level above HRP
forward_yaw = 180, -- +Z forward
yaw_angles = { -50, -30, -10, 10, 30, 50 },
pitch_angles = { -20, 0, 20 },
edge_distances = { 2, 4, 6 },
ground_yaw = { -30, 0, 30 },
}
return TrainConfiglocal Checker = require(game.ReplicatedStorage.AiarenaSdk.CompatibilityChecker)
Checker.report(workspace)local result = CompatibilityChecker.check(workspace)
-- result.compatible = true | false
-- result.score = 0-100
-- result.checks = { { name, status, issues, fix_hint }, ... }
-- result.summary = "human-readable summary"
print(result.summary)| Status | Meaning |
|---|---|
| [PASS] | Requirement met |
| [WARN] | Issue found but the game may still be playable |
| [FAIL] | Critical — not AI-ready |
Preferred: download aiarena-sdk-play.zip and install under ReplicatedStorage. Or copy from packages/aiarena-sdk/src/ (PlayRuntime, HttpBridge, PlayBatch). API shapes: /developers/api. Put the aig_ license in ServerStorage, never a LocalScript. Encode obs on the server; do not yield the physics loop on HTTP. Prefer think interval 0.15–0.2 s with PlayBatch.
Degradation: hold last Hub action ~0.45 s → local lip-safe jump/stay → halt after ~2 s (connection_lost). Contest and TypeSafe directors run on the Hub, not as keys in the place.
-- ServerScriptService.PlayBrain.server.lua
local ServerStorage = game:GetService("ServerStorage")
local PlayRuntime = require(game.ReplicatedStorage.AiarenaSdk.PlayRuntime)
local Policy = require(game.ReplicatedStorage.AiarenaSdk.Policy) -- encode only; no weights
local VisionSystem = require(game.ReplicatedStorage.AiarenaSdk.VisionSystem)
-- Copy VISION from roblox/train-my-ai/src/shared/TrainConfig.lua or inline:
local VISION = {
range = 30,
origin_offset_y = 1.5,
forward_yaw = 180,
yaw_angles = { -50, -30, -10, 10, 30, 50 },
pitch_angles = { -20, 0, 20 },
edge_distances = { 2, 4, 6 },
ground_yaw = { -30, 0, 30 },
}
local license = ServerStorage:WaitForChild("ArenaGameLicense") -- StringValue, aig_…
local runtime = PlayRuntime.new({
hubUrl = "https://aia-hub.metamindinglab.com",
licenseKey = license.Value,
gameId = "your-published-place",
brainId = "hub-brain-uuid",
roomSlug = "obby",
})
local TICK = 0.1
local function spawnLoop(npc)
local humanoid = npc:WaitForChild("Humanoid")
local hrp = npc:WaitForChild("HumanoidRootPart")
task.spawn(function()
while humanoid.Health > 0 do
local vision = VisionSystem.scan(npc, VISION)
local extra = {
grounded = humanoid.FloorMaterial ~= Enum.Material.Air,
falling = hrp.AssemblyLinearVelocity.Y < -2,
}
local obs = Policy.encode({
agents = {
{
y = hrp.Position.Y,
z = hrp.Position.Z,
on_ground = extra.grounded,
checkpoint = npc:GetAttribute("Checkpoint") or 0,
},
},
perception_rays = vision.rays,
}, "obby", extra)
-- apply never waits on HTTP; requestAct is task.spawn inside PlayRuntime
runtime:apply(humanoid, hrp, obs, 16)
task.wait(TICK)
end
end)
endActions: m is 9-way move (0 stay … 8), j jump, f fire. Response JSON is { m, j, f, seq } only.
One aig_ in ServerStorage. One PlayRuntime.new per roster brain_id. Clone SKU templates from ServerStorage (rbxassetid — never Hub FBX). Offset spawns, shared collision, ≤20 AIs via PlayBatch + POST /play/act/batch. Full roster JSON + Lua: Deploy & compete.
Rays see the pad where it is this tick. Jump hang is ~0.5 s, so a still "platform ahead" hit is not enough. The Paths gym publishes a timing cheatsheet the overlay reads; the 32-dim map MLP does not invent it.
| Signal | What the game sets | Why |
|---|---|---|
| JumpNow | Bool pulse ~0.45 s | Pad will be aligned after hang — jump this window |
| JumpKind | on (land) / off (leave) | Two different jumps |
| Pad CFrame | Live world pose each Heartbeat | Aim at the pad, not a static spawn |
| onMover | Rider vs pad AABB | Lip rays hit THIS pad, not the next — ignore while riding |
| Rider carry | Slide HRP by pad delta; keep velocity | Anchored CFrame movers do not physics-carry |
-- Heartbeat on an anchored mover (see GymKit.makeMovingPad)
pad:SetAttribute("JumpNow", inWindow) -- true only in the hang-aligned pulse
pad:SetAttribute("JumpKind", riderOn and "off" or "on")
pad:SetAttribute("TimeUntil", inWindow and (pulseUntil - t) or 9)
-- Carry: CFrame animation does not take the NPC with it
hrp.CFrame = hrp.CFrame + (nowPos - lastPos)
hrp.AssemblyLinearVelocity = Vector3.new(padVelX, hrp.AssemblyLinearVelocity.Y, hrp.AssemblyLinearVelocity.Z)
-- Teacher / overlay reads (BrainRunner extra for room_slug paths):
-- extra.padJumpNow, extra.padJumpKind, extra.padX, extra.padZ, extra.onMover
-- Wait until JumpNow, then jump. Do not halt on the pad (walks off against carry).Live POST /play/act still sends a 32-float map vector with no JumpNow bit. For a published moving-pad course, either pack these extras into your room obs or apply wait/NOW locally as overlay (not map BC).
The room slug is a hint, not a requirement. This project trains obby and aiming in parallel (two chats, two brains). Do not replace obby from an aiming chat, and do not park the course from an obby chat.
| Pitfall | Cause | Fix |
|---|---|---|
| NPC walks on lava without dying | CanCollide=true | CanCollide=false + Touched Health=0 |
| NPC falls through world | Baseplate gone, no pads under gaps | Platforms cover all gaps |
| NPC stuck at spawn | Spawn in hazard or wall | Move spawn to a safe platform |
| NPC can't reach goal | Gaps too wide or heights too tall | Gaps < 12 (train 2–4), heights < 5 |
| NPC tunnels through lava | Lava < 0.5 studs | Make lava 1 stud tall |
| Dies on platform edge | Lava overlaps pad | Lava top at Y=1, below pad surface |
| Can't classify objects | Unrecognized names | Platform_*, Hazard_*, … |
Studio sync: TrainServer ↔ roblox/train-my-ai/src/server/TrainServer.server.lua. Set TrainConfig.CAMPUS_SECRET to Hub ARENA_CAMPUS_SECRET when that env is enabled. Obby pass bar: two jumps and CP 2 while alive — ignore CP printed after DIED!. Course source: roblox/gyms/src/LocomotionEasy.lua.