Menu
CoddyTech

Bot-Zustände

Lektion 16 von 20 im Kurs Roblox-Spiel: Schlachtfelder von Coddy.

Der Verstand des Bots gerät durcheinander. Spiel-KI wird üblicherweise als Zustände geschrieben: Bei jedem Tick wird entschieden, in welchem Zustand sich der Bot befindet, und dann wird ausgeführt, was dieser Zustand vorgibt. Dieser Bot hat fünf:

  • Stunned: Deine Kombo hat ihn eingefroren. Er tut nichts.
  • Retreat: Bei weniger als Settings.RetreatAt seiner Gesundheit läuft er vor dir davon.
  • Idle: Niemand in Sicht.
  • Attack: Du bist in Reichweite.
  • Chase: Du bist in Sicht.

Die Reihenfolge der Prüfungen entspricht den Prioritäten des Bots. Der Zustand ist ein Attribut des Bots, State, sodass du sehen kannst, was er gerade denkt.

challenge icon

Aufgabe

Schwer

Schreibe den Inhalt der Schleife von runBot als Zustände um. Bestimme state in der obigen Reihenfolge und setze das State-Attribut des Bots; dann bewegt sich Chase auf das Ziel zu, Attack bleibt stehen und trifft (wie zuvor), Retreat bewegt sich 12 Studs gerade vom Ziel weg, und alles andere bleibt stehen.

Drücke Play und dann Check.

Probier es selbst

local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local Debris = game:GetService("Debris")
local Settings = require(game.ReplicatedStorage.Settings)

local remotes = game.ReplicatedStorage.Remotes
local bots = workspace.Bots
local lastSwing = {} -- Spieler -> wann er zuletzt zugeschlagen hat
local combo = {} -- Spieler -> { Treffer in Folge, wann der letzte Treffer landete }

-- Roblox’ eigene Schwertanimationen
local slash = Instance.new("Animation")
slash.AnimationId = "rbxassetid://522635514"
local lunge = Instance.new("Animation")
lunge.AnimationId = "rbxassetid://522638767"

print("Swords deal " .. Settings.SwordDamage .. " damage")

-- Eine Zahl, die kurz über der getroffenen Figur schwebt
local function damageNumber(character, amount)
	local root = character:FindFirstChild("HumanoidRootPart")
	if not root then
		return
	end
	local tag = Instance.new("BillboardGui")
	tag.Name = "Damage"
	tag.Size = UDim2.fromOffset(60, 30)
	tag.StudsOffset = Vector3.new(0, 4.5, 0)
	tag.Adornee = root
	local text = Instance.new("TextLabel")
	text.Size = UDim2.fromScale(1, 1)
	text.BackgroundTransparency = 1
	text.TextColor3 = Color3.fromRGB(255, 220, 80)
	text.TextScaled = true
	text.Text = "-" .. math.round(amount)
	text.Parent = tag
	tag.Parent = character
	Debris:AddItem(tag, 0.8)
end

-- Jeder Treffer im Spiel läuft hierüber
local function hurt(humanoid, amount)
	if humanoid.Health <= 0 then
		return
	end
	local character = humanoid.Parent
	-- Beim Blocken wird der Großteil des Treffers abgefangen
	if character:GetAttribute("Blocking") then
		amount *= Settings.BlockMultiplier
	end
	humanoid:TakeDamage(amount)
	damageNumber(character, amount)
end

-- Eine Betäubung macht jemanden kurz bewegungsunfähig
local function stun(humanoid)
	local character = humanoid.Parent
	if character:GetAttribute("Stunned") then
		return
	end
	character:SetAttribute("Stunned", true)
	local speed = humanoid.WalkSpeed
	humanoid.WalkSpeed = 0
	task.delay(Settings.StunTime, function()
		if character.Parent then
			humanoid.WalkSpeed = speed
			character:SetAttribute("Stunned", nil)
		end
	end)
end

-- Ein Schwerthieb: erst die Animation, dann werden alle in der Box vor dir getroffen
local function swing(player)
	local character = player.Character
	local root = character and character:FindFirstChild("HumanoidRootPart")
	if not root or character.Humanoid.Health <= 0 then
		return
	end
	local now = time()
	if lastSwing[player] and now - lastSwing[player] < Settings.SwingCooldown then
		return
	end
	lastSwing[player] = now
	-- Drei Hiebe in Folge ergeben eine Kombo; der dritte ist ein Ausfallschritt
	local c = combo[player]
	if c and now - c.last <= Settings.ComboWindow then
		c.count = c.count % 3 + 1
	else
		c = { count = 1 }
		combo[player] = c
	end
	c.last = now
	local finisher = c.count == 3
	character.Humanoid.Animator:LoadAnimation(finisher and lunge or slash):Play()
	-- Die Box vor dir, niemals dein eigener Körper
	local params = OverlapParams.new()
	params.FilterDescendantsInstances = { character }
	local box = root.CFrame * CFrame.new(0, 0, -3)
	local hit = {} -- Jeder Humanoid nur einmal pro Hieb
	for _, part in workspace:GetPartBoundsInBox(box, Settings.HitboxSize, params) do
		local humanoid = part.Parent:FindFirstChildOfClass("Humanoid")
		if humanoid and not hit[humanoid] then
			hit[humanoid] = true
			hurt(humanoid, Settings.ComboDamage[c.count])
			if finisher then
				stun(humanoid)
			end
		end
	end
end

-- Die Fähigkeiten nach Namen: was jede einzelne bewirkt
local abilities = {}

function abilities.Dash(player, character, root)
	root.AssemblyLinearVelocity = root.CFrame.LookVector * Settings.DashSpeed + Vector3.new(0, 15, 0)
end

-- Alle um dich herum werden getroffen und von dir weggeschleudert
function abilities.Slam(player, character, root)
	local params = OverlapParams.new()
	params.FilterDescendantsInstances = { character }
	local hit = {}
	for _, part in workspace:GetPartBoundsInRadius(root.Position, Settings.SlamRadius, params) do
		local humanoid = part.Parent:FindFirstChildOfClass("Humanoid")
		local theirRoot = part.Parent:FindFirstChild("HumanoidRootPart")
		if humanoid and theirRoot and not hit[humanoid] then
			hit[humanoid] = true
			hurt(humanoid, Settings.SlamDamage)
			local away = (theirRoot.Position - root.Position) * Vector3.new(1, 0, 1)
			if away.Magnitude > 0 then
				away = away.Unit
			end
			theirRoot:ApplyImpulse((away * Settings.SlamKnockback + Vector3.new(0, 40, 0)) * theirRoot.AssemblyMass)
		end
	end
end

function abilities.Block(player, character)
	character:SetAttribute("Blocking", true)
	task.delay(Settings.BlockTime, function()
		if character.Parent then
			character:SetAttribute("Blocking", nil)
		end
	end)
end

-- Der Client fordert eine Fähigkeit anhand ihres Namens an. Der Server prüft: Ist es eine echte
-- Fähigkeit, lebt der Spieler und ist ihre Abklingzeit abgelaufen? Wann sie wieder einsatzbereit ist,
-- steht in einem Attribut des Spielers, damit es auf seinem Bildschirm angezeigt werden kann.
remotes.Ability.OnServerEvent:Connect(function(player, name)
	if typeof(name) ~= "string" or not abilities[name] then
		return
	end
	local character = player.Character
	local root = character and character:FindFirstChild("HumanoidRootPart")
	if not root or character.Humanoid.Health <= 0 then
		return
	end
	local now = workspace:GetServerTimeNow()
	if now < (player:GetAttribute(name .. "ReadyAt") or 0) then
		return
	end
	player:SetAttribute(name .. "ReadyAt", now + Settings.Cooldowns[name])
	abilities[name](player, character, root)
end)

-- Jeder Spieler erhält Kills auf der Bestenliste und in jedem Leben ein Schwert
Players.PlayerAdded:Connect(function(player)
	local leaderstats = Instance.new("Folder")
	leaderstats.Name = "leaderstats"
	leaderstats.Parent = player

	local kills = Instance.new("IntValue")
	kills.Name = "Kills"
	kills.Parent = leaderstats

	player.CharacterAdded:Connect(function()
		local sword = ServerStorage.Sword:Clone()
		sword.Parent = player.Backpack
		sword.Activated:Connect(function()
			swing(player)
		end)
	end)
end)

-- Die Trainingspuppe: Ihr Lebensbalken zeigt ihre Gesundheit an, und eine besiegte
-- Puppe erscheint kurz darauf als neue Kopie an derselben Stelle
local function setupDummy(dummy)
	local humanoid = dummy.Humanoid
	humanoid.HealthChanged:Connect(function(health)
		dummy.HealthBar.Back.Fill.Size = UDim2.fromScale(health / humanoid.MaxHealth, 1)
	end)
	humanoid.Died:Connect(function()
		local spot = dummy:GetPivot()
		task.wait(0.5)
		dummy:Destroy()
		task.wait(Settings.DummyRespawn)
		local fresh = ServerStorage.Dummy:Clone()
		fresh:PivotTo(spot)
		fresh.Parent = workspace
		setupDummy(fresh)
	end)
end
setupDummy(workspace.Dummy)

-- Der Charakter des nächsten lebenden Spielers, den ein Bot sehen kann, und seine Entfernung
local function nearestCharacter(position)
	local best, bestDistance = nil, Settings.BotSight
	for _, player in Players:GetPlayers() do
		local character = player.Character
		local root = character and character:FindFirstChild("HumanoidRootPart")
		if root and character.Humanoid.Health > 0 then
			local distance = (root.Position - position).Magnitude
			if distance < bestDistance then
				best, bestDistance = character, distance
			end
		end
	end
	return best, bestDistance
end

-- Die Steuerung eines Bots: Jede Viertelsekunde schaut er sich um und entscheidet
local function runBot(bot)
	local humanoid = bot.Humanoid
	local root = bot.HumanoidRootPart
	local lastAttack = 0
	while bot.Parent and humanoid.Health > 0 do
		local target, distance = nearestCharacter(root.Position)
		if target and distance <= Settings.BotRange then
			-- Nah genug: anhalten und zuschlagen, einmal pro BotAttackCooldown
			humanoid:MoveTo(root.Position)
			if time() - lastAttack >= Settings.BotAttackCooldown then
				lastAttack = time()
				humanoid.Animator:LoadAnimation(slash):Play()
				hurt(target.Humanoid, Settings.BotDamage)
			end
		elseif target then
			humanoid:MoveTo(target.HumanoidRootPart.Position)
		else
			humanoid:MoveTo(root.Position)
		end
		task.wait(0.25)
	end
end

-- Ein neuer Bot auf der roten Plattform, mit seinen Werten und einem Lebensbalken
local function spawnBot()
	local bot = ServerStorage.Bot:Clone()
	bot:PivotTo(workspace.BotSpawn.CFrame + Vector3.new(0, 3, 0))
	bot.Humanoid.MaxHealth = Settings.BotHealth
	bot.Humanoid.Health = Settings.BotHealth
	bot.Humanoid.WalkSpeed = Settings.BotSpeed
	bot.Parent = bots
	-- Die Physik des Bots auf dem Server belassen, damit der Rückstoß des Slam ihn bewegt
	bot.HumanoidRootPart:SetNetworkOwner(nil)
	bot.Humanoid.HealthChanged:Connect(function(health)
		bot.HealthBar.Back.Fill.Size = UDim2.fromScale(health / bot.Humanoid.MaxHealth, 1)
	end)
	task.spawn(runBot, bot)
end

spawnBot()
quiz iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Roblox-Spiel: Schlachtfelder