AI Script Review

so i’ve been scripting a demogorgon AI; and i would like your feedbacks on how to make my code more efficient, i am not pleased with a few bugs but that is not the point, please tell me how to make my code better and more readable. i think the thing i can see i should improve on is making a module for this, but i’am not very experienced with module scripts so…

--SERVICE
local pathfind = game:GetService("PathfindingService")
local playerservice = game:GetService("Players")
local runservice = game:GetService("RunService")
local heartbeat = runservice.Heartbeat

--OBJECT
local demogorgon = script.Parent
local demohumanoid = demogorgon:WaitForChild("Humanoid")
local rootdemo = demogorgon:WaitForChild("HumanoidRootPart")
local destination

--VARIABLE
local maxrange = 45

local ismoving = false

--TABLE / ETC
local path = pathfind:CreatePath()


--FUNCTION
local function accurateWait(n)
  local elapsed = 0
  while elapsed < n do
    elapsed = elapsed+heartbeat:Wait()
  end
  return elapsed
end

local function chase()
	--compute and get waypoints
	path:ComputeAsync(rootdemo.Position, destination.PrimaryPart.Position)
	local waypoints = path:GetWaypoints()
	--loop through waypoints and visualize
	for _, waypoint in pairs(waypoints) do
		local part = Instance.new("Part")
		part.Shape = "Ball"
		part.Material = "Neon"
		part.Color = Color3.new(255,255,255)
		part.Size = Vector3.new(0.6, 0.6, 0.6)
		part.Position = waypoint.Position
		part.Anchored = true
		part.CanCollide = false
		part.Parent = game.Workspace
		--move to the waypoints
		demohumanoid:MoveTo(waypoint.Position)
		demohumanoid.MoveToFinished:Wait()
	end
end

local function eat(target)
	local humanoid = target:WaitForChild("Humanoid")
	local data = demogorgon:WaitForChild("Data")
	local attacks = data:WaitForChild("Attacks")
	local eatfolder = attacks:WaitForChild("Eat")
	
	local info = {
		["REQUIRES STARTUP"] = true,
		["BRINGS UPSIDE DOWN"] = true,
		["DAMAGE"] = 35	,
		["COOLDOWN"] = 6,
		["ON COOLDOWN"] = false
	}
	
	if info["REQUIRES STARTUP"] == true and info["ON COOLDOWN"] == false then
		humanoid.WalkSpeed = 13
		local startup = demohumanoid:LoadAnimation(eatfolder:WaitForChild("STARTUP"))
		startup:Play()
		info["IS ON COOLDOWN"] = true
		
		startup.Stopped:Connect(function()
			--[[print("ok creating hitbox")
			local hitbox = Instance.new("Part")
			hitbox.Size = Vector3.new(21.55, 3.05, 17.91)
			hitbox.CFrame = rootdemo.CFrame * CFrame.new(0,0,4)
			hitbox.Massless = true
			hitbox.CanCollide = false
			print("finished creating hitbox")
			
			print("creating welds")
			local weld = Instance.new("WeldConstraint")
			weld.Part0 = hitbox
			weld.Part1 = rootdemo
			weld.Parent = hitbox
			print("finished welding")]]--
			print("starting attack sequence")
			humanoid.WalkSpeed = 0
			demohumanoid:LoadAnimation(eatfolder:WaitForChild("ANIM"))
			humanoid:TakeDamage(info["DAMAGE"])
			
			accurateWait(1)
			humanoid.WalkSpeed = 16
			print("waited")
			accurateWait(info["COOLDOWN"])
			print(info["COOLDOWN"])
			print("making cooldown finished")
			info["ON COOLDOWN"] = false
			
		end)
	end
	
end

game:GetService("Players").PlayerAdded:Connect(function(player)
    player.CharacterAppearanceLoaded:Connect(function(character)
        while true do
            local HRP = character:WaitForChild("HumanoidRootPart")
            if (HRP.Position - rootdemo.Position).Magnitude <= maxrange then
				destination = character
				chase()
				print("called chase function and set destination to character")
				if (HRP.Position - rootdemo.Position).Magnitude <= 4 then
					eat(character)
					print("called eat function")
				end
            end
            accurateWait()
        end
    end)
end)

2 Likes

I’m not that good of a scripter myself, but I’d suggest you name your variables/functions better, it will certainly help you with your code and optimize it more :+1:
example:

  • Instead of playerservice do PlayerService
  • Instead of pathfind do PathFindingService
  • instead of ismoving do IsMoving
  • insread of chase() do Chase()
1 Like

i don’t like having to use caps, it gets annoying so i minimize it as much as possible (yes i know hypocritical with the contents of my tables but i just do that because i do?)

and variable names do not optimize nor put latency on anything

1 Like

I acknowledge that, I used to have the same habit of naming my vars firstName or namefirst, but it does make your code clean and more readable.
You can see what experienced scripters do while naming variables on this thread.

also by optimize I meant making it cleaner, mb.

Generally speaking, a good rule of thumb is to make primitives variables lowercase
Ex:

name = "John Doe" -- string
userId = 2 -- number
activeOnRoblox = false -- bool

And use uppercase when working with objects
Ex:

Players = game:GetService('Players')
Player = Players.LocalPlayer

Car = {
  honk = function()
    print('BEEEEP')
  end,
  gas = 6,
}

Or so I’ve been told

3 Likes

Hey there!

I’ve wrote a rendition of this script over a cup of coffee, cutting down on the unnecessary snippets of code that is never executed, making certain actions more verbose where I thought they should be, sprinkling on a bit of my own code style, and interjecting the snippet often for certain suggestions to pursue in your free time.

First, if you have any questions about the following snippet, feel free to ask! Second of all, this is not a prescriptive solution! Code is extremely expressive, so take this snippet at face value; this is just a reference to help you better inform your means of designing code and building up your own style.

-- NOTE: Certain modifications have occured that may need to be patched to work
-- seamlessly with elements outside of this script.

local PathfindingService = game:GetService("PathfindingService");
local Players = game:GetService("Players");
local RunService = game:GetService("RunService");

-- TODO: Inverstigate decoupling demogorgon stats to a config folder or module.
local DEMOGORGON_ATTACK_RANGE = 4;
local DEMOGORGON_COOLDOWN_IN_SECONDS = 6;
local DEMOGORGON_DAMAGE = 35;
local MARKER_COLOR = Color3.fromRGB(255, 255, 255);
local MARKER_SIZE = Vector3.new(0.6, 0.6, 0.6);
local MAX_RANGE_IN_STUDS = 45;
local TARGET_STUN_DURATION_IN_SECONDS = 1;
local TARGET_WALKSPEED_WHILE_ATTACKED = 0;

local demogorgonModel = script.Parent;
local demogorgonConfig = demogorgonModel:WaitForChild("Data");
local demogorgonHumanoid = demogorgonModel:WaitForChild("Humanoid");

-- TODO: Investigate if the default values used for
-- `PathfindingService.CreatePath` are appropriate for the demogorgon model at
-- hand.
local path = PathfindingService:CreatePath();

--[[
    Creates a new marker at the desired position.
    @param {Vector3} position - The position the marker should be created at.
    @returns {Part} - The created marker.
]]
local function createMarker(position)
    assert(typeof(position) == "Vector3");

    local marker = Instance.new("Part");
    marker.Anchored = true;
    marker.CanCollide = false;
    marker.Color = MARKER_COLOR;
    marker.Shape = Enum.PartType.Ball;
    marker.Size = MARKER_SIZE;
    marker.Material = Enum.Material.Neon;
    marker.Parent = workspace;
    marker.Position = position;

    return marker;
end

--[[
    Moves the demogorgon to the desired position. This function is yielding.
    @param {Vector3} position - The position the demogorgon will move to.
    @returns {nil}
]]
local function moveDemogorgonTo(position)
    assert(typeof(position) == "Vector3");

    path:ComputeAsync(demogorgonModel.RootPart.Position, position);
    local waypoints = path:GetWaypoints();

    for _, waypoint in ipairs(waypoints) do
        -- TODO: Inverstigate debug aids, like markers, possibly being
        -- toggleable.
        local marker = createMarker(waypoint.Position);
        demogorgonHumanoid:MoveTo(waypoint.Position);
        demogorgonHumanoid.MoveToFinished:Wait();
    end
end

--[[
    Makes the demogorgon attack the target humanoid model. This function is
    yielding.
    @param {Model} targetHumanoidModel - The humanoid model to attack.
    @returns {nil}
]]
local function attack(targetHumanoidModel)
    assert(typeof(targetHumanoidModel) == "Instance");
    assert(targetHumanoidModel:IsA("Model"));

    local targetHumanoid = targetHumanoidModel:FindFirstChild("Humanoid");
    assert(typeof(targetHumanoid) == "Instance");
    assert(targetHumanoid:IsA("Humanoid"));

    -- TODO: Investigate refactoring these constants to the top of this file if
    -- these assets are static and reliably initialized. Also, investigate if
    -- the demogorgon has any other attacks but "Eat". If not, flatten our
    -- assets and references.
    local attackAssets = demogorgonConfig:FindFirstChild("Attacks");
    local eatAttackAssets = attackAssets:FindFirstChild("Eat");
    -- TODO: Rename "STARTUP" to conform to a uniform style. Also, rename
    -- "STARTUP" as it is called on every attack, making it misleading.
    local eatAttackAnimationAsset = eatAttackAssets:FindFirstChild("STARTUP");

    local eatAttackAnimation = demogorgonHumanoid:LoadAnimation(
        eatAttackAnimationAsset
    );
    
    -- TODO: Decouple logic to another function for increased readability.
    eatAttackAnimationAsset.Stopped:Connect(function()
        local originalTargetWalkspeed = targetHumanoid.WalkSpeed;
        targetHumanoid.WalkSpeed = TARGET_WALKSPEED_WHILE_ATTACKED;
        targetHumanoid:TakeDamage(DEMOGORGON_DAMAGE);
        wait(TARGET_STUN_DURATION_IN_SECONDS);
        targetHumanoid.WalkSpeed = originalTargetWalkspeed;
    end)

    eatAttackAnimation:Play();
    wait(eatAttackAnimation.Length);
end

--[[
    Returns the distance between two Vector3 values.
    @param {Vector3} positionOne
    @param {Vector3} positionTwo
    @returns {number}
]]
-- TODO: Decouple function to a Vector module.
local function getDistance(positionOne, positionTwo)
    assert(typeof(positionOne) == "Vector3");
    assert(typeof(positionTwo) == "Vector3");

    return (positionOne - positionTwo).Magnitude;
end

--[[
    Gets the closest player from the desired position. Can return nil.
    @param {Vector3} position
    @returns {Player | nil}
]]
-- TODO: Decouple function to a Vector module.
local function getClosestPlayer(position)
    assert(typeof(position) == "Vector3");

    local closestPlayer;
    local closestDistance = math.huge;

    for _, player in ipairs(Players:GetPlayers()) do
        local playerPosition = player.Character.PrimaryPart.Position;
        local distance = getDistance(position, playerPosition);
        if distance < closestDistance then
            closestPlayer = player;
        end
    end

    return closestPlayer;
end

--[[
    Gets the closest player from the desired position within a certain radius.
    Can return nil.
    @param {Vector3} position
    @param {number} radius
    @returns {Player | nil}
]]
-- TODO: Decouple function to a Vector module.
local function getClosestPlayerInRadius(position, radius)
    assert(typeof(position) == "Vector3");
    assert(type(radius) == "number");

    local closestPlayer = getClosestPlayer(position);
    local playerPosition = closestPlayer.Character.PrimaryPart.Position;
    local distance = getDistance(position, playerPosition);
    
    return distance < radius and closestPlayer or nil;
end

-- Used as a semantic "debounce" variable.
local isAttacking = false;

--[[
    Manages the demogorgon on every frame, yielding when necessary. As such,
    this function bails out as soon as a case of no operation occurs.
    
    This is designed to be an event listener. Do not "debounce" this function
    without good reason.

    @returns {nil}
]]
-- TODO: Investigate deconstructing the demogorgon AI to be more responsive,
-- always switching to closer targets to make a more fluid motion. This is
-- functional for now.
local function attemptToSeekAndDestroy()
    if isAttacking then
        return;
    end

    local playerToTrack = getClosestPlayerInRadius(
        demogorgonHumanoid.RootPart.Position, 
        MAX_RANGE_IN_STUDS
    );

    if not playerToTrack then
        return;
    end

    moveDemogorgonTo(playerToTrack.Character.PrimaryPart.Positon);

    local newDistance = getDistance(
        demogorgonHumanoid.RootPart.Position,
        playerToTrack.Character.PrimaryPart.Position
    );

    if not newDistance <= DEMOGORGON_ATTACK_RANGE then
        return;
    end

    attack(playerToTrack.Character);
end

RunService.Heartbeat:Connect(attemptToSeekAndDestroy);

Hope this helps!

2 Likes

local Color = Color3.new(255,255,255)
local Size = Vector3.new(0.6, 0.6, 0.6)

local function chase()
	--compute and get waypoints
	path:ComputeAsync(rootdemo.Position, destination.PrimaryPart.Position)
	local waypoints = path:GetWaypoints()
	--loop through waypoints and visualize
	for _, waypoint in pairs(waypoints) do
		local part = Instance.new("Part")
		part.Shape = "Ball"
		part.Material = "Neon"
		part.Color = Color
		part.Size = Size
		part.Position = waypoint.Position
		part.Anchored = true
		part.CanCollide = false
		part.Parent = game.Workspace
		--move to the waypoints
		demohumanoid:MoveTo(waypoint.Position)
		demohumanoid.MoveToFinished:Wait()
	end
end

Small tip for the code to run faster, make the anything.new(23,23,23) items local variables outside of the loop so that it can just quickly read a value, rather than having to create a new one every cycle of the loop

Source, and for more tips: The Basics Of Basic Optimization

1 Like

hey; sorry for the late reply but it does this…

also thank you so much you taught me a new organization of scripting that i’m gonna try using from now on

2 Likes

Good catch! The error was the call to getClosestPlayer from getClosestPlayerInRadius not being supplied a parameter.

This is why assertions at the top of each function in a dynamic language like Lua is really useful. In the thrown error, the traceback tells you what parameter is invalid and where the invalid call was invoked (which sadly was cropped out by your screenshot).

Keep in mind that some modifications will have to occur to be seamless with your current codebase; however, this error was not a fault of that, but my own.

(I have edited my original post to reflect the patch)


Thanks for reaching out! If you have any more questions, feel free to reply either here or—if they’re of a different matter entirely—always feel free to directly message me on the devforums!

2 Likes