• Check out the results of the Techtree Contest #19!
  • Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.
  • Create a void inspired texture for Warcraft 3 and enter Hive's 34th Texturing Contest: Void! Click here to enter!
  • The Hive's 22nd Icon Contest: Creep Abilities is now concluded, time to vote for your favourite set of icons! Click here to vote!

Procedural unit spawn in an area

Level 6
Joined
Jul 3, 2006
Messages
102
For a forest area of my map, I would like to have complete procedural unit generation, revival and so on.
I would use a region but the shape is shown below.
Are there any other tricks/systems people use for such circumstances?
I thought about placing invisible cues throughout and using them as locations.

1768040826755.png
 
I had ChatGPT whip up some code real quick, this allows you to create your own shapes with up to 100 points. (It could be more, just increase Array size)
vJASS:
    function CreateTriangle takes nothing returns nothing
        local Polygon pol = Polygon.create()
        // Triangle
        call pol.addPoint(Point.create(-500.0, 0.0))
        call pol.addPoint(Point.create(500.0, 0.0))
        call pol.addPoint(Point.create(0.0, 1000.0))
        call pol.finish()
        // Track it globally
        set LastPolygon = pol
    endfunction
vJASS:
library Polygons

    globals
        Polygon LastPolygon
    endglobals

    struct Point
        real x
        real y
        static method create takes real x, real y returns thistype
            local thistype this = thistype.allocate()
            set this.x = x
            set this.y = y
            return this
        endmethod
    endstruct

    struct Polygon
        Point array points[99]
        integer count = 0
        real minX
        real maxX
        real minY
        real maxY

        method addPoint takes Point p returns nothing
            set this.count = this.count + 1
            set this.points[this.count] = p
        endmethod

        method finish takes nothing returns nothing
            local real array out
            local integer i = 1
            local Point p = this.points[1]
 
            // Calculate bounds
            set out[0] = p.x
            set out[1] = p.x
            set out[2] = p.y
            set out[3] = p.y
            loop
                exitwhen i > this.count
                set p = this.points[i]
                if p.x < out[0] then
                    set out[0] = p.x
                endif
                if p.x > out[1] then
                    set out[1] = p.x
                endif
                if p.y < out[2] then
                    set out[2] = p.y
                endif
                if p.y > out[3] then
                    set out[3] = p.y
                endif
                set i = i + 1
            endloop
            set this.minX = out[0]
            set this.maxX = out[1]
            set this.minY = out[2]
            set this.maxY = out[3]
        endmethod
    endstruct

    function DistSq takes real x1, real y1, real x2, real y2 returns real
        local real dx = x2 - x1
        local real dy = y2 - y1
        return dx*dx + dy*dy
    endfunction

    function IsInPolygon takes Point p, Polygon poly returns boolean
        local real minX
        local real maxX
        local real minY
        local real maxY
        local integer i
        local integer j
        local boolean inside = false
        local Point qi
        local Point qj
        if poly.count < 3 then
            return false
        endif
        // --- Bounding box check (optional optimization)
        set minX = poly.points[1].x
        set maxX = poly.points[1].x
        set minY = poly.points[1].y
        set maxY = poly.points[1].y
        set i = 1
        loop
            exitwhen i > poly.count
            set qi = poly.points[i]
            if qi.x < minX then
                set minX = qi.x
            endif
            if qi.x > maxX then
                set maxX = qi.x
            endif
            if qi.y < minY then
                set minY = qi.y
            endif
            if qi.y > maxY then
                set maxY = qi.y
            endif
            set i = i + 1
        endloop
        if p.x < minX or p.x > maxX or p.y < minY or p.y > maxY then
            return false
        endif
        // --- pnpoly logic
        set j = poly.count
        set i = 1
        loop
            exitwhen i > poly.count
            set qi = poly.points[i]
            set qj = poly.points[j]
            if ((qi.y > p.y) != (qj.y > p.y)) and (p.x < (qj.x - qi.x) * (p.y - qi.y) / (qj.y - qi.y) + qi.x) then
                set inside = not inside
            endif
            set j = i
            set i = i + 1
        endloop
        return inside
    endfunction
 
    //////////////////////////////////////////
    // TEST TRIANGLE                        //
    // REPLACE WITH YOUR OWN SHAPES + LOGIC //
    //////////////////////////////////////////

    function CreateTriangle takes nothing returns nothing
        local Polygon pol = Polygon.create()
        // Triangle
        call pol.addPoint(Point.create(-500.0, 0.0))
        call pol.addPoint(Point.create(500.0, 0.0))
        call pol.addPoint(Point.create(0.0, 1000.0))
        call pol.finish()
        // Track it globally
        set LastPolygon = pol
    endfunction

    function TestTriangle takes real x, real y returns nothing
        local Point p = Point.create(x, y)
        if IsInPolygon(p, LastPolygon) then
            call BJDebugMsg("XY is inside polygon")
        else
            call BJDebugMsg("XY is outside polygon")
        endif
    endfunction

    function CreateUnitsInTriangle takes integer count, real minDistance returns boolean
        local integer created = 0
        local integer attempts
        local integer maxAttempts = count * 25  // failsafe
        local real minDistSq = minDistance * minDistance // minimum dist between units
        local real x
        local real y
        local integer i
        local boolean valid
        local Point p
 
        // Store accepted spawn points
        local real array spawnX
        local real array spawnY
 
        loop
            exitwhen created >= count
 
            set attempts = 0
            set valid = false
 
            loop
                exitwhen attempts >= maxAttempts or valid
 
                // Random point in bounding box
                set x = GetRandomReal(LastPolygon.minX, LastPolygon.maxX)
                set y = GetRandomReal(LastPolygon.minY, LastPolygon.maxY)
 
                set p = Point.create(x, y)
 
                if IsInPolygon(p, LastPolygon) then
                    set valid = true
                    set i = 0
                    loop
                        exitwhen i >= created
                        if DistSq(x, y, spawnX[i], spawnY[i]) < minDistSq then
                            set valid = false
                            exitwhen true
                        endif
                        set i = i + 1
                    endloop
                endif
 
                set attempts = attempts + 1
            endloop
 
            // Failsafe: impossible placement
            if not valid then
                call BJDebugMsg("CreateUnitsInPolygon failed: not enough space")
                return false
            endif
 
            // Accept point
            set spawnX[created] = x
            set spawnY[created] = y
 
            call CreateUnit(Player(0), 'hfoo', x, y, 0.0)
 
            set created = created + 1
        endloop
 
        return true
    endfunction
 
endlibrary
I attached a map with some example code for spawning units within a triangle shape. It also enforces a minimum distance between each of them so that you can separate the spawns. You can change it to a shape that matches your picture above and adjust the code to create "spawn points" or whatever instead of Footman.
vJASS:
            call CreateUnit(Player(0), 'hfoo', x, y, 0.0) // Create anything you want at x/y
 

Attachments

Last edited:
Could also have a few regions to get "close enough" shape (having like 3-5 regions can get pretty close), and select one at random.
Chance of selecting a region should be proportional to its size if you want an even distribution. But probably that isn't super important.

When creating a unit in an unpathable location, it will be "moved" to closest valid location, so regions are not required to be perfect either
 
I had ChatGPT whip up some code real quick, this allows you to create your own shapes with up to 100 points. (It could be more, just increase Array size)

This was my initial thought as well, but this approach would require a way to set up the polygon vertices interactively in the World Editor. Afaik the best way to do this would be with some sort of invisible model (units/destructables). Then, the question becomes, why not use these invisible cues to do the spawning instead of a polygon region?

This functionality can probably be useful in all other sorts of things though.
 
One thing about spawning units at random [x,y] coordinates is that units won't always be spaced out evenly across the region. Quite often you will encounter situations where most units are spawned cluttered around same part of a region and few spawned at other parts.

On the other hand, while using min distance as a prerequisite for picking unit's spawn point will resolve the above issue (Uncle's script is good example of that), it may lead to situation where unit won't respawn at all, because there just won't be any free place to respawn it.
Let's say you want min distance to be 100 range. By nature of randomness, you may select 3 locations which are spaced ~190 distance from each other. That satisfies the 100 min range requirement, but now you have 3 locations that have quite a lot of free space in between them, but that space is just not large enough to fit another spawn point there.
You may for example want to keep respawning 90 creeps around 100 locations in the rect you've shown in your original post, but because of the spacing + randomness factor, one game may fit in there only ~70 locations, while other ~95 locations, etc.
To counter that, you would need to decrease the min distance requirement, but then you get back to the original issue with cluttering :D

Honestly, I would just use units for this. Create a custom unit that will serve as a spawn point. Give it movement type Fly and 0 pathing size so that it does not collide with anything else.
A good model for such dummy could be the 'Start Location' model under 'Extra' category. That model at scale 1.0 has ~180.00 radius, so it could serve as a good visual cue for a potential spawn area.
If only we had natives for accessing destructible's scale, then I would propose using destructibles with variable min/max scale as spawn points, but alas, no such natives exist AFAIK.
Anyway, place the dummy units in your map in areas that you want to serve as spawn points. On map initialization pick all those dummy units, get their location and store them in a point array global variable. Finally, remove those dummy units.

When you want to spawn a unit, rather than spawn it exactly at picked location, you can spawn it within random distance (between 0 and 180.00) and random angle from the picked location. That way you would utilize the fully area that the "Start Location" model is taking.

What you do with those locations is then up to you - if you want to spawn only a single enemy or group of enemies at that location and then make that spawn location unavailable until the enemy/group of enemies die; or just make given spawn point unavailable for choosing for X seconds since it last spawned minions, then you could use the array swapping method, where you keep available location at the start of the array and also keep track of how many are available. Then pick randomly one of the available locations, spawn minion and then swap the location with last available location and decrease number of available locations by 1.
Something like this pseudo-code:
Code:
let spawnLocations = [loc1, loc2, ..., loc10]
let availableLocations = 10

// pick location randomly
let pickedIndex = rand(1, availableLocations)
let pickedLoc = spawnLocations[ pickedIndex ]

// swap picked location with last available location
set spawnLocations[pickedIndex] = spawnLocations[availableLocations]
set spawnLocations[availableLocations] = pickedLoc

// decrease available locations by 1
set availableLocations -= 1

of course, if you don't care for that, you can just completely randomly pick one location using let pickedLoc = spawnLocations[ rand(1, availableLocations) ] without ever decreasing availableLocations variable.
 
This was my initial thought as well, but this approach would require a way to set up the polygon vertices interactively in the World Editor. Afaik the best way to do this would be with some sort of invisible model (units/destructables). Then, the question becomes, why not use these invisible cues to do the spawning instead of a polygon region?

This functionality can probably be useful in all other sorts of things though.
The system uses x/y coordinates, so you don't need any kind of object to represent them. Move your mouse to a vertice, then look at the bottom left corner of the World Editor and you'll see "Point: X, Y, Z". Write each of those X/Y values down in Notepad.

It looks like your shape has about 10 vertices, so it shouldn't take more than a few minutes.

The benefits:
1. It's bloat free, everything is contained within a single script.
2. The Polygons, which are basically custom shaped Regions, can be stored and used throughout the entire game.
3. You can run functions designed to interact with a Polygon, to ease the overall process of your goal: In this case procedural generation.
I have an example of procedurally generating Footman from within a triangle shaped area.

Of course you can do whatever you'd like, I just figured this was a clean and expandable approach.
 
Last edited:
The system uses x/y coordinates, so you don't need any kind of object to represent them. Move your mouse to a vertice, then look at the bottom left corner of the World Editor and you'll see "Point: X, Y, Z". Write each of those X/Y values down in Notepad.

It looks like your shape has about 10 vertices, so it shouldn't take more than a few minutes.

The benefits:
1. It's bloat free, everything is contained within a single script.
2. The Polygons, which are basically custom shaped Regions, can be stored and used throughout the entire game.
3. You can run functions designed to interact with a Polygon, to ease the overall process of your goal: In this case procedural generation.
I have an example of procedurally generating Footman from within a triangle shaped area.

Of course you can do whatever you'd like, I just figured this was a clean and expandable approach.
If he has more regions later, it would requite a lot of work
of course, if you don't care for that, you can just completely randomly pick one location using let pickedLoc = spawnLocations[ rand(1, availableLocations) ] without ever decreasing availableLocations variable.
Better use destructibles, smaller lag than units, if he needs to place thousands of spawn points across multiple regions
 
The system uses x/y coordinates, so you don't need any kind of object to represent them. Move your mouse to a vertice, then look at the bottom left corner of the World Editor and you'll see "Point: X, Y, Z". Write each of those X/Y values down in Notepad.

It looks like your shape has about 10 vertices, so it shouldn't take more than a few minutes.
Don't get me wrong, I can think of many ways I would use polygon shaped regions, it is just not practical having to manually set up the vertices and if it changes later, setting them up again. This would be a great addition to the editor as a function.

For my purposes I have currently created 2 destructables without a model and placed them around the forest area. I use a loop to gather them in an array at initialisation and then loop though all of them to spawn either 1-2 mobs in the 1st type and 2-3 in the 2nd type (like a creep camp). I use random distance/angle from their location to fake a scattering.
 
Back
Top