Roblox Studioで敵AIを作る:プレイヤーを発見して追跡する方法
AshKnightが40 studs以内の生存プレイヤーを見つけ、障害物を避けて6 studsまで追跡するserver AIを作ろう。

前の記事のTrainingDummyを、近くの生存プレイヤーを見つけて追うAshKnightへ発展させる。40 studs以内で追跡を始め、壁を回り、6 studsまで近づいたら攻撃せずに止まる。

1. AshKnightと障害物を置く
TrainingDummyをAshKnightへ変える。Humanoid.MaxHealthとHealthを150、WalkSpeedを10にし、HumanoidRootPart.Anchoredをオフにする。rigのすべてのPartが床へめり込まず、rootが地面から約3 studs上にある位置へ移そう。
combat-02で横へ退避したTestWallを選び、PathfindingObstacleへ名前を変える。新しい壁を増やす必要はない。Size = 12, 7, 2、Position = 0, 3.5, 1へ変え、AnchoredとCanCollideをオンにする。AshKnightとCombatSpawnの間で真っすぐな進路を遮るため、経路計算が左右へ曲がる様子を確認できる。
Tip — navigation meshは「歩ける場所の地図」
Studioは床、壁、段差からAIが歩ける面を計算する。高さ5、半径2 studsのagentに合わせてpathを作ると、AshKnightの身体が通れるwaypointを得られる。
2. serverで一番近い生存プレイヤーを探す
AshKnightへScriptを追加し、AshKnightServerと名付ける。次のcodeへ置き換えよう。
local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local npc = script.Parent
local humanoid = npc:WaitForChild("Humanoid")
local root = npc:WaitForChild("HumanoidRootPart")
local DETECTION_RANGE = 40
local STOP_DISTANCE = 6
local STOP_TOLERANCE = 0.35
local REPATH_DELAY = 0.2
for _, item in npc:GetDescendants() do
if item:IsA("BasePart") and not item.Anchored then
item:SetNetworkOwner(nil)
end
end
local function setState(state, targetName, distance)
npc:SetAttribute("AIState", state)
npc:SetAttribute("TargetName", targetName or "")
npc:SetAttribute("TargetDistance", distance or -1)
end
local function nearestTarget()
local nearest
local nearestDistance = DETECTION_RANGE
for _, player in Players:GetPlayers() do
local character = player.Character
local targetHumanoid = character
and character:FindFirstChildOfClass("Humanoid")
local targetRoot = character
and character:FindFirstChild("HumanoidRootPart")
if targetHumanoid and targetHumanoid.Health > 0 and targetRoot then
local distance = (targetRoot.Position - root.Position).Magnitude
if distance <= nearestDistance then
nearest = character
nearestDistance = distance
end
end
end
return nearest, nearestDistance
end
local function nextWaypoint(targetPosition)
local path = PathfindingService:CreatePath({
AgentRadius = 2,
AgentHeight = 5,
AgentCanJump = false,
WaypointSpacing = 4,
})
local ok = pcall(function()
path:ComputeAsync(root.Position, targetPosition)
end)
if not ok or path.Status ~= Enum.PathStatus.Success then
npc:SetAttribute("PathStatus", "NO_PATH")
return nil
end
npc:SetAttribute("PathStatus", "SUCCESS")
path.Blocked:Once(function()
npc:SetAttribute("PathStatus", "BLOCKED_REPATH")
humanoid:MoveTo(root.Position)
end)
local waypoints = path:GetWaypoints()
return waypoints[2] and waypoints[2].Position or targetPosition
end
setState("IDLE")
while humanoid.Health > 0 do
local target, distance = nearestTarget()
if not target then
setState("IDLE")
npc:SetAttribute("MoveStatus", "HOLD")
humanoid.WalkSpeed = 0
humanoid:MoveTo(root.Position)
task.wait(REPATH_DELAY)
continue
end
local targetRoot = target:FindFirstChild("HumanoidRootPart")
if not targetRoot then
task.wait(REPATH_DELAY)
continue
end
if distance <= STOP_DISTANCE + STOP_TOLERANCE then
setState("CHASE", target.Name, distance)
npc:SetAttribute("MoveStatus", "HOLD")
humanoid.WalkSpeed = 0
root.AssemblyLinearVelocity = Vector3.zero
humanoid:MoveTo(root.Position)
task.wait(REPATH_DELAY)
continue
end
setState("CHASE", target.Name, distance)
humanoid.WalkSpeed = 10
local offset = targetRoot.Position - root.Position
local desired = targetRoot.Position
- offset.Unit * (STOP_DISTANCE - 1)
local waypoint = nextWaypoint(desired)
if waypoint then
npc:SetAttribute("MoveStatus", "MOVING")
humanoid:MoveTo(waypoint)
else
npc:SetAttribute("MoveStatus", "NO_PATH")
humanoid:MoveTo(root.Position)
end
task.wait(REPATH_DELAY)
end
SetNetworkOwner(nil)でNPCの物理計算をserverへ固定する。clientごとに別の位置を正本にしないためだ。対象選択もserverで行い、clientからNPCの座標や対象名を受け取らない。
Tip — waypointはpath上の次の目印
GetWaypoints()は目的地までの曲がり角を順番に返す。このcodeは毎回2番目へMoveTo()し、0.2秒後に新しいプレイヤー位置からpathを作り直す。
Path.Blockedが発生したら一度停止し、次のloopで経路を再計算する。壁が増えた後も古いwaypointへ押し続けない。
stateをAttributeで見えるようにする
AIState、MoveStatus、PathStatusは、追跡の判断をExplorerから読めるようにするAttributeだ。対象がいなければIDLE / HOLD、追跡中はCHASE / MOVING、距離へ到達するとCHASE / HOLDになる。path計算が成功したときはPathStatus = SUCCESSだ。
Attributeは移動を実行する命令ではない。Humanoid:MoveTo()とWalkSpeedが実際の動きを変え、Attributeは現在の判断を観察するために使う。この分離により、画面で止まっている理由が「対象なし」「距離へ到達」「pathなし」のどれかを調べやすい。
nearestTarget()は毎回Playersを読み直し、Healthが0より大きいCharacterだけを候補にする。複数clientのtestでも一番近い生存targetを選び、1人目の名前を固定しない。最終ゲームを1人serverにしても、別clientへstateを誤適用しないことを先に確認できる。
3. 6 studs手前を目的地にする
プレイヤーの中心までpathを引くと、NPCが身体へぶつかる。desiredは対象からSTOP_DISTANCE - 1だけ手前だ。実距離が6.35以下になったら速度を0にするため、最終表示はほぼ6 studsになる。

Tip — repathは動く対象へ経路を引き直すこと
playerは動くため、最初のpathだけでは古い場所へ向かう。今回は
REPATH_DELAY = 0.2、つまり1秒に最大5回だけ計算し、毎frameの重い計算を避ける。
4. 追跡、停止、対象なしを確認する
PlayしてAshKnightから40 studs以内へ入る。障害物の正面で止まらず、左右どちらかのwaypointへ向かえばpathが成功している。
AshKnightが約6 studsで止まり、プレイヤーへ重ならないことを見る。

最後に、40 studsより外へ離れるか、ResetでプレイヤーのHealthを0にする。AIStateがIDLE、MoveStatusがHOLDになり、その場で止まればよい。

敵が小刻みに震える場合は、STOP_TOLERANCEを消していないか、停止中にもWalkSpeed = 10を設定していないか確認する。壁へ押し続ける場合はPath.Blockedの接続とREPATH_DELAYを読み直そう。
pathが作れないとき
AshKnightがNO_PATHのままなら、床と障害物のCanCollideがオンか、rigの脚が床へ深く埋まっていないかを確認する。障害物とarenaの端の間には、半径2 studsのagentが通れる幅を残す。細い隙間を見た目だけで通れると判断せず、障害物を1〜2 studs内側へ動かして再度Playしよう。
追跡範囲はroot同士の直線距離だ。床に沿ったpathの長さではない。AshKnightの近くでTargetDistanceが40以下になってからCHASEへ変わり、40を超えると次の0.2秒周期でIDLEへ戻ることをAttributeでも確認できる。
最後はAshKnightが6 studsで止まる状態に戻して保存しよう。これが次の開始状態だ。
これでAshKnightは攻撃距離まで自分で移動できる。次は6 studsで止まった後に0.8秒の予備動作を見せ、その後だけ35ダメージを与えるboss attackを追加する。