PcoWSkbVqDnWTu_dm2ix
We use cookies on this site to enhance your user experience

Top Down Action: PiBot

Top Down Action: PiBot

Jul 30 2018, 2:30 PM PST 10 min

The first type of enemy the player will encounter in the game is the PiBot. These robots will attack the player on sight and will give chase if the player tries to run away.

TDS_PiBotPromo.png

Right now our PiBots don’t move or act, so let’s get them moving. Each robot will need a script to control its actions. There are a fair number of robots in the game, so instead of manually inserting a script into each one, let’s write a script to insert scripts automatically.

Insert a Script into ServerScriptService and name it AIScriptGiver. Any script put into ServerScriptService runs as soon as the game runs, so it is a good alternative place to Workspace to store scripts. Insert the following code into AIScriptGiver:

local robotScripts = game.ServerStorage.RobotScripts

local function insertAIScript(robot)
	local aiScript = robotScripts:FindFirstChild(robot.Name):Clone()
	aiScript.Parent = robot
end

local function onRobotAdded(robot)
	insertAIScript(robot)
end

for _, robot in pairs(game.Workspace.Robots:GetChildren()) do
	insertAIScript(robot)
end

game.Workspace.Robots.ChildAdded:connect(onRobotAdded)

We first setup a variable for the RobotScripts Folder inside of ServerStorage. If you look inside that Folder, you’ll notice several empty scripts with the same name as the robot models. The function insertAIScript copies a script from the RobotScripts Folder with the same name as the passed in robot model. It then puts the copied script into the passed in robot.

The onRobotAdded function is setup to insert scripts into any new robots that get added to the game, and the for loop inserts scripts into all of the robots already in the game.

PiBot Script

The PiBot is the basic opponent in the game and is the first robot players will face. When they see a player, they move closer to that player and launch pies. Let’s get started on their script which will implement that behavior. Open the PiBot Script in the RobotScripts Folder in ServerStorage and enter the following code:

local robot = script.Parent

local configurations = {}

configurations.AggroDistance = 50
configurations.ActionDistance = 20
configurations.ActionCooldown = 2
configurations.LastAction = 0

configurations.Aggro = function(target)
	
end

configurations.Action = function()
	
end

local function onDied()
	
end

robot.Humanoid.Died:connect(onDied)

We start off by just laying the framework for our robot. The configurations table will later be passed into a generic AI controller which will help our robot operate. This table contains some variables to configure the behavior of the robot, along with the functions Aggro and Action. These actions will determine what the PiBot will do when it first sees the player and when it gets close to the player respectively. We also bind the function onDied to the robot’s Humanoid/Died|Died event which we will use later for cleanup.

Let’s fill in these functions to let the robot know what to do when these functions are called. When the Aggro function is called, the robot should move towards its target. When the Action function is called, it should stop moving and activate the Tool it is holding to throw a pie at the player.

local robot = script.Parent
local humanoid = robot.Humanoid
local walkTrack = humanoid:LoadAnimation(robot.WalkAnimation)

local configurations = {}

configurations.AggroDistance = 50
configurations.ActionDistance = 20
configurations.ActionCooldown = 2
configurations.LastAction = 0


configurations.Aggro = function(target)
	humanoid:MoveTo(target.Torso.Position)
	if not walkTrack.IsPlaying then
		walkTrack:Play()
	end
end

configurations.Action = function()
	humanoid:MoveTo(robot.Torso.Position)
	robot.Tool:Activate()
	walkTrack:Stop()
end

local function onDied()
	wait(2)
	robot:Destroy()
end

robot.Humanoid.Died:connect(onDied)

We first load a walking animation into the robot’s humanoid. In the Aggro function, which is called when the robot sees a player, we tell the robot to move towards its target with the humanoid Humanoid/MoveTo|MoveTo function. Also, if the walking animation hasn’t started yet, we play it. In the Action function, we stop the robot from moving by setting the destination of the MoveTo function to the current position of the robot. We then activate the tool the robot is holding and stop the walking animation. In the onDied event, we simply wait two seconds before destroying the robot, which removes it from the workspace.

Robot Controller

Now that we have defined what our PiBot will do, let’s add code to determine when to perform its various actions. While we could include this code into the script we just wrote, let’s instead put it into a ModuleScript so that it can be used by other robots that we will make later. In the RobotScripts Folder inside of ServerStorage, insert a new ModuleScript and name it RobotController. Insert the following code into it:

local RobotController = {}

function RobotController:RunAI(robot, configurations)
	
end

return RobotController

We used a ModuleScript earlier when making the PieLauncher. In that case, we used it for variables that could be shared between scripts. In this case, we are using it to share a function: RunAI. Every robot script that we make in the game will all call RunAI to help figure out when it should perform its actions. The function takes two arguments: first the robot, which is the model of the robot that is being controlled, and a table configurations which has the Aggro and Action functions of the robot, along with some settings.

Let’s add in the logic to help the robot decide what to do:

local RobotController = {}

local updateDelta = .1

local function getClosestVisibleCharacter(robot)
	
end

function RobotController:RunAI(robot, configurations)
	while wait(updateDelta) and robot.Humanoid.Health > 0 do
		local target, targetDistance = getClosestVisibleCharacter(robot)
		if target then
			if targetDistance < configurations.ActionDistance then
				configurations.Action(target)
			elseif targetDistance < configurations.AggroDistance then
				configurations.Aggro(target)
			end
		end
	end
end

return RobotController

The RunAI function starts a loop that repeats every 1/10th of a second and stops if the robot doesn’t have health (if the robot is knocked out, it doesn’t need to act anymore). In this loop, we first get the closest visible target to the robot. If there is a target, then we check how far away that target is. If the target is within action distance, then we call the robot’s Action function. Otherwise, if the robot is inside of its aggro distance, we call the Aggro function. If the target is outside of either of those ranges, or if there isn’t a visible target at all, the robot will simply not do anything.

Closest Visible Character

Now we need to define the getClosestVisibleCharacter function to let the robot know which player it will target. To help out, we’ll first define a function to get the distance to a passed in player character.

local RobotController = {}

local updateDelta = .1

local function distanceToCharacter(robot, character)
	if not character or character.Humanoid.Health <= 0 then return nil end
	local toPlayer = character.Head.Position - robot.Head.Position
	local toPlayerRay = Ray.new(robot.Head.Position, toPlayer)
	local part = game.Workspace:FindPartOnRay(toPlayerRay, robot, false, false)
	if part and part:IsDescendantOf(character) then
		return toPlayer.magnitude
	end
	return nil
end

local function getClosestVisibleCharacter(robot)
	
end

function RobotController:RunAI(robot, configurations)
	while wait(updateDelta) and robot.Humanoid.Health > 0 do
		local target, targetDistance = getClosestVisibleCharacter(robot)
		if target then
			if targetDistance < configurations.ActionDistance then
				configurations.Action(target)
			elseif targetDistance < configurations.AggroDistance then
				configurations.Aggro(target)
			end
		end
	end
end

return RobotController

The distanceToCharacter function first checks if the player’s character exists at all and still has health. If not, then we return nil as we don’t want the robot targeting that player. We get the direction to the player from the robot by subtracting the position of the player’s head from the position of the robot’s head. We then create a DataType/Ray|Ray pointing from the robot to the character using the direction we just calculated. We use this Ray in Workspace/FindPartOnRay|FindPartOnRay to see if there are any parts between the robot and character’s heads. If the raycast finds a part that is part of the player’s character, then we know there is nothing between the characters and we can return the distance between them.

With our helper function distanceToCharacter, we can now implement getClosestVisibleCharacter:

local RobotController = {}

local updateDelta = .1

local function distanceToCharacter(robot, character)
	if not character or character.Humanoid.Health <= 0 then return nil end
	local toPlayer = character.Head.Position - robot.Head.Position
	local toPlayerRay = Ray.new(robot.Head.Position, toPlayer)
	local part = game.Workspace:FindPartOnRay(toPlayerRay, robot, false, false)
	if part and part:IsDescendantOf(character) then
		return toPlayer.magnitude
	end
	return nil
end

local function getClosestVisibleCharacter(robot)
	local closestDistance = math.huge
	local closestCharacter = nil
	for _, player in pairs(game.Players:GetPlayers()) do
		local distance = distanceToCharacter(robot, player.Character)
		if distance and distance < closestDistance then
			closestDistance = distance
			closestCharacter = player.Character
		end
	end
	return closestCharacter, closestDistance
end

function RobotController:RunAI(robot, configurations)
	while wait(updateDelta) and robot.Humanoid.Health > 0 do
		local target, targetDistance = getClosestVisibleCharacter(robot)
		if target then
			if targetDistance < configurations.ActionDistance then
				configurations.Action(target)
			elseif targetDistance < configurations.AggroDistance then
				configurations.Aggro(target)
			end
		end
	end
end

return RobotController

We start off by creating variables for the closest distance and character. The variable closestDistance is set to math.huge so the first value we compare against it will always be smaller. Then, we cycle through every player in the game and check the distance to that character using distanceToCharacter. If that distance is less than the current closestDistance, that player becomes the closest character. We finish by returning closestCharacter and closestDistance.

Hooking up the PiBot

Our controller isn’t quite done, but let’s hook it up to our PiBot script so we can see some of the behaviors in action. Modify the PiBot script with the following code:

local robot = script.Parent
local humanoid = robot.Humanoid
local walkTrack = humanoid:LoadAnimation(robot.WalkAnimation)
local robotController = require(game.ServerStorage.RobotScripts.RobotController)

local configurations = {}

configurations.AggroDistance = 50
configurations.ActionDistance = 20
configurations.ActionCooldown = 2
configurations.LastAction = 0

configurations.Aggro = function(target)
	humanoid:MoveTo(target.Torso.Position)
	if not walkTrack.IsPlaying then
		walkTrack:Play()
	end
end

configurations.Action = function()
	humanoid:MoveTo(robot.Torso.Position)
	robot.Tool:Activate()
	walkTrack:Stop()
end

local function onDied()
	wait(2)
	robot:Destroy()
end

robot.Humanoid.Died:connect(onDied)

robotController:RunAI(robot, configurations)

Now if we run our game the PiBots will chase our character and stop chasing if it gets too close.

Face player

Right now the robots will move towards the player but do not turn. We will use a BodyGyro|BodyGyro like we did in the player character to orient the robots in the correct direction. Update RobotController with the following code:

local RobotController = {}

local updateDelta = .1

local function distanceToCharacter(robot, character)
	if not character or character.Humanoid.Health <= 0 then return nil end
	local toPlayer = character.Head.Position - robot.Head.Position
	local toPlayerRay = Ray.new(robot.Head.Position, toPlayer)
	local part = game.Workspace:FindPartOnRay(toPlayerRay, robot, false, false)
	if part and part:IsDescendantOf(character) then
		return toPlayer.magnitude
	end
	return nil
end

local function getClosestVisibleCharacter(robot)
	local closestDistance = math.huge
	local closestCharacter = nil
	for _, player in pairs(game.Players:GetPlayers()) do
		local distance = distanceToCharacter(robot, player.Character)
		if distance and distance < closestDistance then
			closestDistance = distance
			closestCharacter = player.Character
		end
	end
	return closestCharacter, closestDistance
end

local function orientRobot(robot, target)
	local torso = robot.Torso
	local targetTorso = target.Torso
	torso.BodyGyro.CFrame = CFrame.new(torso.Position, targetTorso.Position)
end

function RobotController:RunAI(robot, configurations)
	while wait(updateDelta) and robot.Humanoid.Health > 0 do
		local target, targetDistance = getClosestVisibleCharacter(robot)
		if target then
			orientRobot(robot, target)
			if targetDistance < configurations.ActionDistance then
				configurations.Action(target)
			elseif targetDistance < configurations.AggroDistance then
				configurations.Aggro(target)
			end
		end
	end
end

return RobotController

Now if a robot has a target, it will call the orientRobot function. This function points the BodyGyro|BodyGyro that exists in all of the level’s robots to face towards the player.

TDS_PiBot.png