How could I achieve Capture Objective / King of The Hill?

image
the script is enabled
image

edit:

Ive resolved this by changing the code to

local hitbox = workspace:GetPartBoundsInBox(workspace.map1.Zone.CFrame . . .

i expected you to do the line of code that’d go zone.Cframe, zone.Size, sorry

:Touched() is generally not reliable. The only real use it has is when you have hundreds of parts that need to do something when touched, like an obby.

@Amritss, how could i put the code into checking if the player is in team blue or team red or team lobby now? i tried it by checking the localplayers team and then printing it out but it seemed not to work

	for i, part in hitbox do -- :GetPartBoundsInBox returns an array of baseparts detected so we can loop through them
		if part.Parent:FindFirstChildOfClass("Humanoid") then
			if part.Parent:FindFirstChild(localplayer == game.Teams.Blue) then
				print("blue")
				else print("not blue")
			end
		end
	end

i’ve tried this aswell and it doesn’t work

for i, part in hitbox do -- :GetPartBoundsInBox returns an array of baseparts detected so we can loop through them
if part.Parent:FindFirstChildOfClass("Humanoid") then
local char = part.Parent
local player = game.Players:GetPlayerFromCharacter(char)
if player.Team = game.Teams.Blue then
print("blue")
else
print("not blue")
			end
		end
	end

I realised my mistake earlier and saw you wanted it on the zone, I guess you didn’t see the edit.

prints out “Not blue” no matter the team

I tried using the Touched event for this and it wasn’t consistent. That’s why I started to use the spatial queries.

Basically, the KotH sensor is a model (or Actor if using Parallel LUA). Within that model you put the actual sensor plate and the script. The script runs the spatial query every 0.1 seconds or so. The methods to use are Workspace:GetPartsBoundInBox() and Workspace:GetPartBoundsInRadius. You don’t need to query 50 studs high, just a height of about 10 studs max for performance reasons. It returns a table of parts in the region. Just check if the parts belong to one or more players. If they do, then check the teams of the players. If the teams are the same, then color the sensor plate the TeamColor. If not, then color the sensor plate the neutral color.

Another script will run with a while true do loop timed every few seconds to check the plate color. If the color is not the neutral color, then increment the score of the color who’s team it is and display it.

EDIT:

The zone library that someone posted is good for what it does, but it doesn’t really work for a King of the Hill sensor. It’s just for announcing to the player what zone they are in. With that being said, you will have to do a spatial query to see who is on the plate.

1 Like
local zone = workspace.map1.Zone -- This is just a part from the workspace you can use to visualise the hitbox
local Runservice = game:GetService("RunService")
while true do
	local hitbox = workspace:GetPartBoundsInBox(zone.CFrame, zone.Size) -- look up the syntax on documentation
	for i, part in hitbox do -- :GetPartBoundsInBox returns an array of baseparts detected so we can loop through them
		if part.Parent:FindFirstChildOfClass("Humanoid") then
			local char = part.Parent
			local player = game.Players:GetPlayerFromCharacter(char)
			if player.Team == game.Teams.Blue then
				print("blue")
			else
				print("not blue")
			end
		end
	end

	Runservice.Heartbeat:Wait() -- Just so that we don't crash.
end

this is the current code i have, however it doesnt print out the correct terms when the team is blue it just always prints out not blue

how else do you think I should approach this?

On the face of it, I do not see anything really wrong with your code, but there are a couple of issues. Is the player actually a member of the Blue team?

  1. The way that you are running the query is not entirely correct. You are running the query within the part itself and not on the surface of it. However, if the part has collisions off, then this would be valid.

  2. You are running the routine on the heartbeat. That’s going to pull quite a bit of the server’s CPU bandwidth and may lag out the server if you have other things going on. I recommend using a 0.1 second delay for this. You could get away with a 0.25 second delay.

  3. Instead of checking for a specific team, check if the player is on a team and then print that team name. Then you can see what’s going on.

Try this code:

-- ******** Requirements

-- Required Game Services and Facilities
local playerService = game:GetService("Players")



-- ******** Local Data

local zone = script.Parent
local teamValue = zone:FindFirstChild("Team")
local defaultColor = BrickColor.new("Medium Stone Grey")
local spatialQuery = game.Workspace.GetPartBoundsInBox
local playerQuery = playerService:GetPlayerFromCharacter



-- ******** Functions/Methods

-- Returns the player, character, humanoid, and humanoid root part
-- (if any) that is associated with the given part.
local function getPlayerFromHit(hitPart: Instance): (Player, Model, Humanoid, BasePart)
	-- Setup
	local player = nil
	local char = nil
	local human = nil
	local root = nil

	-- Process
	char = hitPart:FindFirstAncestorOfClass("Model")
	if char ~= nil then
		human = char:FindFirstChild("Humanoid")
		root = char:FindFirstChild("HumanoidRootPart")
		if human ~= nil and root ~= nil then
			player = playerQuery(char)
		end
	end

	-- Return Results
	return player, char, human, root
end



-- ******** Initialize

-- If the team object value does not exist, then create it.
if teamValue == nil then
	teamValue = Instance.new("ObjectValue")
	teamValue.Name = "Team"
	teamValue.Parent = zone
end



-- ******** Run

-- Forks off a separate thread that runs forever which
-- performs a spatial query to get all players that are
-- on the sensor plate and aggregate the teams.  The
-- number of teams are then counted and the plate
-- color is adjusted accordingly.
task.spawn(function()
	-- Setup
	local zoneCFrame = zone:GetPivot()
	local zoneSize = Vector3.new(zone.Size.X, 10, zone.Size.Z)

	-- Operational
	local teamList
	local teamVal
	local count
	local color
	local player

	-- Process
	while true do
		-- First we get all players that are on the KotH sensor plate.  There is no
		-- need to check for duplicates because we are always setting the team
		-- entry in the table to true and we are not counting the number of players
		-- on each team.
		teamList = {}
		local hitbox = spatialQuery(zoneCFrame, zoneSize)
		for _, item in ipairs(hitbox) do
			player = getPlayerFromHit(item)
			if player ~= nil then
				if player.Team ~= nil then
					teamList[player.Team] = true
				end
			end
		end

		-- Count the number of teams encountered.  If it's != 1 then we set
		-- the default color, otherwise we set it to the team color.  If we do
		-- set a team color, we also set the TeamValue object value to that
		-- teams so the monitoring script can score correctly.
		count = 0
		for team, _ in pairs(teamList) do
			count += 1
			color = team.TeamColor
			teamVal = team
		end
		if count == 1 then
			zone.BrickColor = color
			teamValue.Value = teamVal
		else
			zone.BrickColor = defaultColor
			teamValue.Value = nil
		end

		-- Added for debugging purposes.
		print(teamValue.Value, zone.BrickColor)

		task.wait(0.1)
	end
end)

The above code was made from memory and off the top of my head. If the player is not on a team, then they are ignored. If they are on a team, the a table entry is set to true. I didn’t check for duplicates for performance reasons because more steps are required to check, and it doesn’t sure anything repeatedly setting the same table entry to true.

The second part checks for the number of teams in the table. If there’s no team or more than one team, then the default color is set. If only one team is present, then that team color is used. You were specifically checking for a team which is not good practice. This is more generic and will work with any number of teams without modification.

An alternative would be to search the team list array and then add the team using table.insert() if it’s not there. Then you can use #teamList to get the number of entries in the table, but I’m not sure of the performance impact of that. You want this to run as fast as possible which is why I deployed a number of tricks to get the most performance out of it.

The scoring/round script looks something like this:

-- ******** Requirements

local playerService = game:GetService("Players")
local replicatedStorage = game:GetService("ReplicatedStorage")
local teamService = game:GetService("Teams")



-- ******** Local Data

-- Remote Events
local eventFolder = replicatedStorage:FindFirstChild("Events")
local eventScore = eventFolder:FindFirstChild("Score")
local eventTimer = eventFolder:FindFirstChild("Timer")
local eventWinner = eventFolder:FindFirstChild("Winner")

-- Setup
local zone = game.Workspace.map1.Zone
local teamValue = zone:FindFirstChild("Team")

-- Settings
local defaultColor = BrickColor.new("Medium Stone Grey")
local scoreGoal = 100
local checkInterval = 5
local roundTime = 600

-- Operational
local endRound = false
local score = {}



-- ******** Functions/Methods

-- Sends a command to one or more clients.
local function sendCommand(event: RemoteEvent, player: Player,
	command: number, d1: any, d2: any, d3: any, d4: any, d5: any)
	if player == nil then
		event:FireAllClients(command, d1, d2, d3, d4, d5)
	else
		event:FireClient(player, command, d1, d2, d3, d4, d5)
	end
end

-- Updates the score display for all players.
local function updateScoreDisplay(score: table)
	-- Do whatever that needs to be done here, which usually
	-- involves a RemoteEvent:FireAllClients() to send the data
	-- to the client for processing.
	sendCommand(eventScore, nil, 0, score)
end

-- Updates the timer display for all players.
local function updateTimerDisplay(counter: number)
	-- Do whatever that needs to be done here, which usually
	-- involves a RemoteEvent:FireAllClients() to send the data
	-- to the client for processing.
	sendCommand(eventTimer, nil, 0, counter)
end

-- Displays the winning player, or tie on the player's screen.
local function displayWinner(data: table)
	-- Do whatever that needs to be done here, which usually
	-- involves a RemoteEvent:FireAllClients() to send the data
	-- to the client for processing.
	if data.tie == true then
		-- We have a tie.
		sendCommand(eventWinner, nil, 2, data)
	else
		-- We have a winner.
		local playerList = playerService:GetPlayers()
		for _, player in ipairs(playerList) do
			if player.Team == data.team then
				-- Winning Player
				sendCommand(eventWinner, player, 1, data)
			else
				-- Losing Player
				sendCommand(eventWinner, player, 0, data)
			end
		end
	end
end

-- Determines if there was a winner or a tie.
local function determineWinner(scoreTable: table)
	-- Setup
	local data = {
		team = nil;
		tie = false;
	}
	-- Find the team with the highest score.
	local maxTeam = nil
	local maxScore = -math.huge
	for team, score in pairs(scoreTable) do
		if score > maxScore then
			maxTeam = team
			maxScore = score
		end
	end

	-- Now that we know what the highest score is, we check if
	-- another team has the same score.  If they do, then we have
	-- a tie.
	local count = 0
	for _, score in pairs(scoreTable) do
		if score == maxScore then
			count += 1
		end
	end
	if count == 1 then
		-- There can be only *1*, and that's the winning team.
		data.team = maxTeam
	else
		-- If the count is not one, then we have a tie.
		data.tie = true
	end

	-- Return Result
	return data
end



-- ******** Initialize

-- Create a score entry for each team that exists.
do
	local teamList = teamService:GetTeams()
	for _, team in ipairs(teamList) do
		score[team] = 0
	end
end



-- ******** Run

-- Periodically checks the Team object value of the KotH sensor plate
-- for a valid team.  If one is set, then that team's score in increased.
task.spawn(function()
	-- Setup
	local color
	local team
	local start

	-- Process
	while endRound == false do
		-- Waits for the specified interval before checking if there is
		-- a team on the plate.  This is a precision wait that takes the
		-- loop execution time into account.
		task.wait(checkInterval - (os.clock() - start))

		-- Check if the round ended while we were waiting.  If it did,
		-- then terminate thread execution **NOW**.
		if endRound == true then
			return
		end

		-- Process
		start = os.clock()
		team = teamValue.Value
		if team ~= nil then
			if type(score[team]) == "number" then
				score[team] += 1
			end
		end

		-- Updates the score display for all players.
		updateScoreDisplay(score)

		-- Check if any of the team scores have reached the goal
		-- score.
		for _, value in pairs(score) do
			if value >= scoreGoal then
				endRound = true
			end
		end
	end
end)

-- This is the round timer loop.  This executes on its own thread
-- and runs every second.  This loop employs a precision wait
-- that is based on the loop execution time.
task.spawn(function()
	local counter = roundTime
	local start

	-- This is the round timer loop.  This loop will only exit on two conditions:
	-- 1. The endRound becomes true for whatever reason.
	-- 2. The counter becomes 0.
	while endRound == false and counter > 0 do
		start = os.clock()
		counter -= 1
		updateTimerDisplay(counter)
		task.wait(1 - (os.clock() - start))
	end

	-- Set the endRound to true so all running loops will exit.
	endRound = true

	-- Determine if there was a tie or winner.  If a winner is declared,
	-- then reward them.
	local winData = determineWinner(score)
	if winData.tie == false then
		-- Reward players on the winning team.
		local playerList = winData.team:GetPlayers()
		for _, player in ipairs(playerList) do
			-- Reward Players
		end
	end
	displayWinner(winData)

	-- From here, do whatever needs to be done which can include teleporting
	-- players to a lobby, time delay to start a new round, etc....
end)

This is pretty much an entire King of the Hill system. You’ll have to adapt the code for your uses, but this is kind of what I use in my game. The big thing about this is what to do at the end of the round. I know that some games have the lobby and the map in the same place. Mine doesn’t, so I don’t bother with resetting everything to defaults after the round ends.

How you handle your client displays is entirely up to you, but mine implements a command system where the server sends a command to the client. On the client, the command numbers index into a table of function references so it gets called when the server sends that command and the data is passed though.

As I said before, all of this came from memory and from the top of my head, so there’s probably some typos and syntax errors in it.

So basically, the way this works is that after initialization, there’s two loops that run on their own threads which do different things. The first loop (scoring) runs every few seconds to check the sensor plate for players and updates the score accordingly. The second loop (timer) runs every second and handles the round clock. The timer is primary and when it exits, it performs some end of round functions like determining who the winner is, if any, rewarding the players on the winning team, displaying the winner/tie, etc…

EDIT: After proofreading it, I found a couple of errors, so you’ll have to copy the code again.

2 Likes

thank you for providing me with all the codes and the information

There is an error that I’ve resolved, instead of playerService:GetPlayerFromCharacter It has to be playerService:GetPlayerFromCharacter
however the code still doesnt change the color of anything at all

the part has collisions off and this is how its shaped:
image

should this code work on default? I have run it and It doesn’t do anything for me with no errors (in the dev console) aswell
image

image
(i suppose this is how we fix the tie error?
image )

image

image

probably because i forgot to put the two equal signs for the if statement.

I’ve seen that, upon fixing that it still didn’t work properly

How is that possible… Your studio is cursed my friend, do you have canquery off or something.

nope it is on
image

local zone = workspace.map1.Zone -- This is just a part from the workspace you can use to visualise the hitbox
local Runservice = game:GetService("RunService")
while true do
	local hitbox = workspace:GetPartBoundsInBox(workspace.map1.Zone.CFrame, zone.Size) -- look up the syntax on documentation
	for i, part in hitbox do -- :GetPartBoundsInBox returns an array of baseparts detected so we can loop through them
		if part.Parent:FindFirstChildOfClass("Humanoid") then
			for i, part in hitbox do -- :GetPartBoundsInBox returns an array of baseparts detected so we can loop through them
				if part.Parent:FindFirstChildOfClass("Humanoid") then
					local char = part.Parent
					local player = game.Players:GetPlayerFromCharacter(char)
					if player.Team == game.Teams.Blue then
						print("blue")
					else
						print("not blue")
					end
				end
			end
		end
	end
	Runservice.Heartbeat:Wait() -- Just so that we don't crash.
end

image

Is the part anchored? Cuz it would fall into the void otherwise

yes the part is anchored
image

I fixed the errors that you pointed out.

The one thing that I can think of is your transparency setting. If it’s 1, then nothing will show because it’s fully transparent.

EDIT:

Wait, is that a part or a NEGATIVE part? The coloring looks like it’s a negative part. You cannot set the color on a negative part because it will not show.

I’ll test this from my side and update you on what to do.

@Maelstorm_1973
after changing the code to the after edit code, it still doesnt do anything…

I’m not sure what a negative part is, it is the default part that’d come up upon clicking the part button in the studio interface just colored to a different color to see the zone easier for now,
the transparency is 0.65

edit: upon checking what a negative part is, the zone is not a negative part, it doesnt have any option on the solid modeling selected

I’m lost here, as I don’t know why is nothing printing out

edit: also, is this a error or not?
image
after changing the code to this:
local replicatedStorage = game:GetService("ReplicatedStorage")
the output doesnt show anymore, instead it shows this


this is the 13th line

local eventScore = eventFolder:FindFirstChild("Score")

Just tested it right now, it literally works…