Validate and Reposition Ground Units algorithm

[ADDED] UTILS.ValidateAndRepositionGroundUnits
[ADDED] SPAWN:InitValidateAndRepositionGroundUnits
[ADDED] CTLD.validateAndRepositionUnits
This commit is contained in:
smiki 2025-08-24 15:07:27 +02:00
parent b9cf1e46af
commit 67cb844550
3 changed files with 151 additions and 2 deletions

View File

@ -1049,6 +1049,22 @@ function SPAWN:InitSetUnitAbsolutePositions(Positions)
return self
end
--- Uses Disposition and other fallback logic to find better ground positions for ground units.
--- NOTE: This is not a spawn randomizer.
--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area.
--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit.
-- @param #boolean OnOff Enable/disable the feature.
-- @param #number MaxRadius (Optional) Max radius to search for valid ground locations in meters. Default is double the max radius of the units.
-- @param #number Spacing (Optional) Minimum spacing between units in meters. Default is 5% of the search radius or 5 meters, whichever is larger.
-- @return #SPAWN
function SPAWN:InitValidateAndRepositionGroundUnits(OnOff, MaxRadius, Spacing)
self.SpawnValidateAndRepositionGroundUnits = OnOff
self.SpawnValidateAndRepositionGroundUnitsRadius = MaxRadius
self.SpawnValidateAndRepositionGroundUnitsSpacing = Spacing
return self
end
--- This method is rather complicated to understand. But I'll try to explain.
-- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor,
-- but they will all follow the same Template route and have the same prefix name.
@ -1829,7 +1845,13 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth )
if self.SpawnHiddenOnMap then
SpawnTemplate.hidden=self.SpawnHiddenOnMap
end
if self.SpawnValidateAndRepositionGroundUnits then
local units = SpawnTemplate.units
local gPos = { x = SpawnTemplate.x, y = SpawnTemplate.y }
UTILS.ValidateAndRepositionGroundUnits(gPos, units, self.SpawnValidateAndRepositionGroundUnitsRadius, self.SpawnValidateAndRepositionGroundUnitsSpacing)
end
-- Set country, coalition and category.
SpawnTemplate.CategoryID = self.SpawnInitCategory or SpawnTemplate.CategoryID
SpawnTemplate.CountryID = self.SpawnInitCountry or SpawnTemplate.CountryID

View File

@ -1564,7 +1564,9 @@ function CTLD:New(Coalition, Prefixes, Alias)
self.FixedMinAngels = 165 -- for troop/cargo drop via chute
self.FixedMaxAngels = 2000 -- for troop/cargo drop via chute
self.FixedMaxSpeed = 77 -- 280 kph or 150kn eq 77 mps
self.validateAndRepositionUnits = false -- 280 kph or 150kn eq 77 mps
-- message suppression
self.suppressmessages = false
@ -3735,6 +3737,7 @@ function CTLD:_UnloadTroops(Group, Unit)
self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
:InitDelayOff()
:InitSetUnitAbsolutePositions(Positions)
:InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
:OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end)
:SpawnFromVec2(randomcoord:GetVec2())
self:__TroopsDeployed(1, Group, Unit, self.DroppedTroops[self.TroopCounter],type)
@ -4181,11 +4184,13 @@ function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation,Mult
self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
--:InitRandomizeUnits(true,20,2)
:InitDelayOff()
:InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
:OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end)
:SpawnFromVec2(randomcoord)
else -- don't random position of e.g. SAM units build as FOB
self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
:InitDelayOff()
:InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
:OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end)
:SpawnFromVec2(randomcoord)
end
@ -5211,6 +5216,7 @@ function CTLD:_UnloadSingleTroopByID(Group, Unit, chunkID)
self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template, alias)
:InitDelayOff()
:InitSetUnitAbsolutePositions(Positions)
:InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
:OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end)
:SpawnFromVec2(randomcoord:GetVec2())
self:__TroopsDeployed(1, Group, Unit, self.DroppedTroops[self.TroopCounter], cType)

View File

@ -4871,3 +4871,124 @@ function UTILS.FindNearestPointOnCircle(Vec1,Radius,Vec2)
return {x=qx, y=qy}
end
--- This function uses Disposition and other fallback logic to find better ground positions for a group of ground units.
--- NOTE: This is not a spawn randomizer.
--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area and modifies the provided positions table.
--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit.
-- @param table Positions A table of DCS#Vec2 or DCS#Vec3, can be a units table from the group template.
-- @param DCS#Vec2 Anchor (Optional) DCS#Vec2 or DCS#Vec3 as anchor point to calculate offset of the units.
-- @param #number MaxRadius (Optional) Max radius to search for valid ground locations in meters. Default is double the max radius of the units.
-- @param #number Spacing (Optional) Minimum spacing between units in meters. Default is 5% of the search radius or 5 meters, whichever is larger.
function UTILS.ValidateAndRepositionGroundUnits(Anchor, Positions, MaxRadius, Spacing)
local units = Positions
Anchor = Anchor or UTILS.GetCenterPoint(units)
local gPos = { x = Anchor.x, y = Anchor.z or Anchor.y }
local maxRadius = 0
local unitCount = 0
for _, unit in pairs(units) do
local pos = { x = unit.x, y = unit.z or unit.y }
local dist = UTILS.VecDist2D(pos, gPos)
if dist > maxRadius then
maxRadius = dist
end
unitCount = unitCount + 1
end
maxRadius = MaxRadius or math.max(maxRadius * 2, 10)
local spacing = Spacing or math.max(maxRadius * 0.05, 5)
if unitCount > 0 and maxRadius > 5 then
local spots = UTILS.GetSimpleZones(UTILS.Vec2toVec3(gPos), maxRadius, spacing, 1000)
if spots and #spots > 0 then
local validSpots = {}
for _, spot in pairs(spots) do -- Disposition sometimes returns points on roads, hence this filter.
if land.getSurfaceType(spot) == land.SurfaceType.LAND then
table.insert(validSpots, spot)
end
end
spots = validSpots
end
local step = spacing
for _, unit in pairs(units) do
local pos = { x = unit.x, y = unit.z or unit.y }
local isOnLand = land.getSurfaceType(pos) == land.SurfaceType.LAND
local isValid = false
if spots and #spots > 0 then
local si = 1
local sid = 0
local closestDist = 100000000
local closestSpot
for _, spot in pairs(spots) do
local dist = UTILS.VecDist2D(pos, spot)
if dist < closestDist then
local skip = false
for _, unit2 in pairs(units) do
local pos2 = { x = unit2.x, y = unit2.z or unit2.y }
local dist2 = UTILS.VecDist2D(spot, pos2)
if dist2 < spacing and isOnLand then
skip = true
break
end
end
if not skip then
closestDist = dist
closestSpot = spot
sid = si
end
end
si = si + 1
end
if closestSpot then
if closestDist >= spacing then
pos = closestSpot
end
isValid = true
table.remove(spots, sid)
end
end
-- Failsafe calculation
if not isValid and not isOnLand then
local h = UTILS.HdgTo(pos, gPos)
local retries = 0
while not isValid and retries < 500 do
local dist = UTILS.VecDist2D(pos, gPos)
pos = UTILS.Vec2Translate(pos, step, h)
local skip = false
for _, unit2 in pairs(units) do
if unit ~= unit2 then
local pos2 = { x = unit2.x, y = unit2.z or unit2.y }
local dist2 = UTILS.VecDist2D(pos, pos2)
if dist2 < 12 then
isValid = false
skip = true
break
end
end
end
if not skip and dist > step and land.getSurfaceType(pos) == land.SurfaceType.LAND then
isValid = true
break
elseif dist <= step then
break
end
retries = retries + 1
end
end
if isValid then
unit.x = pos.x
if unit.z then
unit.z = pos.y
else
unit.y = pos.y
end
end
end
end
end