Making a "Capture Point" entity

I know how to make entites, but I want to make a TF2-like capture point entity. How would I do this? I need help. I don’t have much time to post, but I’ll add on to this and be more specific tomorrow.

You need to give the capture point a nextupdate and points variable. Then in the think check if CurTime() > nextupdate and if it is use ents.FindInSphere to find who’s there and add points as required.
Then just check each team’s points (or do it on a positive/negative scale) to set which team the control point is on. Then set nextupdate to CurTime() + 1 or something.

The reason for using a nextupdate system instead of every tick is that ents.FindInSphere is expensive.

Get ready, because this is going to be a long post.

You wouldn’t need an entity, but a think hook which monitors how long a player is present in the box coordinates set.

I’m currently making a gamemode that runs by the same principles of using capture points.

If you go in to the room or open space you want to be the capture point, go to any top corner of the room or box (noclip), and when your feet are not visible in the room anymore, type in to the console “getpos”,
then go in to the lower opposite side of the room and type getpos again.

You should have two pieces of red text, each starting with “setpos”

Anything past where it would say “setang”, ignore.

After the setpos should be three numbers which should look like this:

Take the first three numbers (in this case, 703.4, -209.3 and -79.1) and express them as a single vector for the first set of coordinates as follows:


local hPoint1 = Vector(703.4,-209.3,-79.1)

And do the same for the second triplet of numbers;


local hPoint2 = Vector(14.2,-140.2,0.3)

Overall, you should have two vectors localized next to eachother;


local hPoint1 = Vector(703.4,-209.3,-79.1)
local hPoint2 = Vector(14.2,-140.2,0.3)

Assuming (for now) that you only want to make one capture point, keep these variables local or we will add them to a table of coordinates later.

Make a new Think hook which counts the time a player of a certain team is within the box boundaries, first construct the hook;


hook.Add("Think", "m_hkCheckCapturepointProgress", function()
local hPoint1 = Vector(703.4,-209.3,-79.1)
local hPoint2 = Vector(14.2,-140.2,0.3)
end)

Then we should localize a table of entities contained within the box, so, assuming you localized the coordinates in order of height (top to bottom), you add this in to the hook:

In case you’re unaware, ents.Find functions return tables of entities found within radii consistent with their arguments, in this case, tbEnts is a table of all entities found in a box starting from hPoint2 and ending at hPoint1.


hook.Add("Think", "m_hkCheckCapturepointProgress", function()
local hPoint1 = Vector(703.4,-209.3,-79.1)
local hPoint2 = Vector(14.2,-140.2,0.3)
local tbEnts = ents.FindInBox(hPoint2,hPoint1)
end)

Then we iterate through tbEnts using an ipair loop:


for _,pl in ipairs(tbEnts) do

end

Assuming your capturing team is enumerated (for example, TEAM_EXAMPLE), we add a conditional operator:


for _,pl in ipairs(tbEnts) do
   if pl:IsPlayer() then //Check if pl is a player for sure.
      if pl:Team() == TEAM_EXAMPLE then //Seeing if the player's team is equal to the capturing team.

      end
   end
end

And here’s where it gets a bit tricky… finding the right time measurement to go by, since Think doesn’t run once a second, we cannot make it count up the time in seconds without some math, since I’m not much of a mathhead (and I assume this is the standard method of doing things), we use a starting value, globalized before the hook.Add, for example:


tmCaptured = 0

Then we add a variable to tell us when the last time we updated tmCaptured was, also starting at zero:


tmSinceLastCaptureUpdate = 0

Add a variable which will be the time a player must be in the point for the capture to succeed;


tmCaptureLimit = 10

We should also make a float that we can change at the top of the code which determines how long we should wait in the Think before telling the game to update the capture progress again;


flUpdateTime = 0.2

0.2 is just a rough estimate.

And just a capture check;


bCaptured = false

We should now incorporate that time code in to the loop, the code added says only to do what is between the “then” and the “end” operators if the time since the last update is over the time we should take to get to the next update, we will also add the bCaptured as a boolean to see if we should still try to let players capture the point.


for _,pl in ipairs(tbEnts) do
   if pl:IsPlayer() then //Check if pl is a player for sure.
      if !bCaptured && (CurTime() - tmSinceLastCaptureUpdate > flUpdateTime) && pl:Team() == TEAM_EXAMPLE then
         
      end
   end
end

Now we get to setting the variables, or rather resetting them…

Between the “then” and “end” operators, put “tmSinceLastCaptureUpdate = CurTime()”, this sets the time the game last updated the capture progress to right this moment.

Afterwards, write tmCapture = tmCapture + 0.3, again, 0.3 is just an estimate, you can swap and change the numbers until you get a code that counts to seconds, but it will take a while, depending on what time measurement you’re hoping to use.

After the two variables are set, write the following:


 if tmCapture >= tmCaptureLimit then
OnPointsCaptured()
end

This will give you errors if you run it right away, this is because OnPointsCaptured isn’t defined, so you need to make a new function;


 function OnPointsCaptured()

end

In it, you’ll want to make bCaptured equal true to stop counting the capture time, we also should reset all values back to 0;


 function OnPointsCaptured()
   bCaptured = true
   tmCapture = 0
   tmSinceLastCaptureUpdate = 0
end

You may want to do a timer that waits for about 5 seconds before respawning all players at their spawnpoints and resetting the game… a timer.Simple can manage this;


timer.Simple(5, function()
   for k,v in ipairs(player.GetAll()) do
      v:Spawn() //Respawn them at home.
      bCaptured = false
   end
end)

Finally, add a break operator to your code, just after the end that comes after “OnPointsCaptured()”, just to prevent any problems.

Overall, your code should look a bit like this:


tmCaptured = 0
tmSinceLastCaptureUpdate = 0
tmCaptureLimit = 10
flUpdateTime = 0.2

hook.Add("Think", "m_hkCheckCapturepointProgress", function()
local hPoint1 = Vector(703.4,-209.3,-79.1)
local hPoint2 = Vector(14.2,-140.2,0.3)
local tbEnts = ents.FindInBox(hPoint2,hPoint1)

   for _,pl in ipairs(tbEnts) do
      if pl:IsPlayer() then //Check if pl is a player for sure.
         if !bCaptured && (CurTime() - tmSinceLastCaptureUpdate > flUpdateTime) && pl:Team() == TEAM_EXAMPLE then
            tmSinceLastCaptureUpdate = CurTime()
            tmCaptured = tmCaptured + 0.3

            if tmCaptured >= tmCaptureLimit then
              OnPointsCaptured()
            end; break

         end
      end
   end
end)

function OnPointsCaptured()
   bCaptured = true
   tmCaptured = 0
   tmSinceLastCaptureUpdate = 0
 
  timer.Simple(5, function()
     for k,v in ipairs(player.GetAll()) do
         v:Spawn() //Respawn them at home.
         bCaptured = false
      end
   end)
end


Apologies if I fucked this up at all, I’m writing code without being able to test it at half past one in the morning, so I may have messed up somewhere, but I can’t tell, hopefully it is all explained well enough.

Edit:
I’ll be offline after I’ve posted this, if you reply to me, I may reply at a later time.

Performance wise, wouldn’t it be cheaper to loop through player.GetAll() and check the position rather than using ents.FindInBox?

I assume that due to the fact ents.FindInBox is an engine function, it’ll perform faster than manually doing this in Lua

Indeed it is quite costly on the system, the ents.FindInBox function does cause moderate levels of lag, but it really depends on how you use it in relation to other functions, I suppose iterating through each player to see if they’re contained within a box would be more efficient, but I’m yet to know how to check if a player is contained in a box, the answer I gave is merely a temporary one which can be modified or added to at any time, I didn’t intend on that being final.

At least, that is the system that I used for my gamemode, I was following the same principles for the example since it worked well for me.

It would be indeed. FindIn* functions are slow as hell.

If these points are not ever rotated, then you can do this with pure logic, something like:

aabb are the min and max of box 1, ccdd are for box 2.


function util.AABBIntersects( aa, bb, cc, dd )
  if bb.x < cc.x then return false end
  if aa.x > dd.x then return false end
  if bb.y < cc.y then return false end
  if aa.y > dd.y then return false end
  if bb.z < cc.z then return false end
  if aa.z > dd.z then return false end
end

If your points are going to rotate / be at an angle other than 0, I highly recommend just creating a simple trigger entity.


function ENT:Initialize( )
  self:SetModel( ... )
  self.Touchers = { }
  
  if CLIENT then
    self:SetRenderBounds( min, max )
  else
    self:PhysicsInitBox( min, max )
    self:SetCollisionBounds( min, max )
    self:SetMoveType( MOVETYPE_NONE )
    self:SetSolid( SOLID_OBB )
    self:SetTrigger( true )

    if phys:IsValid( ) then
      phys:SetMass( 1 )
      phys:Wake( )
      phys:SetBuoyancyRatio( 0 )
      phys:EnableGravity( false )
      phys:AddGameFlag( FVPHYSICS_NO_IMPACT_DMG )
      phys:AddGameFlag( FVPHYSICS_NO_NPC_IMPACT_DMG )
      phys:SetMaterial( "gmod_silent" )
    end
  end

  ...
end

function ENT:StartTouch( ent )
  if ent:IsPlayer( ) or ent:IsNPC( ) then
    self.Touchers[ ent ] = true 
  end
end

function ENT:StopTouch( ent )
  self.Touchers[ ent ] = nil
end

function ENT:Think( )
  --Touchers logic, track time in point, if all of them are in the same team, etc
end

Sometimes the entity free solution is useful, but if you want to do anything more complicated than non-rotated square regions, it becomes very problematic. OBBs can be rotated without having to manually calculate if two convex polygons are intersecting, and net you other interesting side effects like being able to grow and shrink them, move them, rotate them, destroy / disable them if they are dealt certain types of damage, make them require a certain amount of weight be on them, etc without significant extra work on your part.

You can remove the physics init part and just use SOLID_BOX for the solid type, the Entity Trigger stuff works with hull traces, so a physics object is not needed at all, that’s how all triggers work in source.