• 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.

Midnighters Pathing System

This bundle is marked as awaiting update. A staff member has requested changes to it before it can be approved.
  • Like
Reactions: Panda

Introduction​


After searching I noticed there weren't many (if any) pathing systems on Hive, so I decided to write my own. I was on the fence of whether to release it or not, but after a bit of consideration, I decided I should. Someone may find it useful for a Tower Defense, AoS and more.

This system facilitates the creation and management of region-based paths for units in a game.

Details​

Paths are divided into segments, each containing up to 12 regions (Found an issue with Wc3 and queuing actions). Units can follow these paths segment by segment.

Main Commands:
1. RegionPathSystem:AddRegionToPath(pathID, region)
- Adds a region to the specified path. Regions are grouped into segments with a max size of 12.
- Automatically creates a new segment if needed and sets up triggers for seamless transitions.
- Example Usage:
RegionPathSystem:AddRegionToPath("PathName", gg_rct_REGIONNAME)

2. RegionPathSystem:SetPathPlayer(pathID, player)
- Associates a path with a specific player. Only units owned by this player can use the path.
- Example Usage:
RegionPathSystem:SetPathPlayer("PathName", Player(0))

3. SetupRegionEntryTrigger(pathID, firstRegion)
- Sets up a trigger for the first region of the path or segment. When a unit enters this region, it begins following the path.
- Automatically converts rects to regions if necessary.
- Example Usage:
SetupRegionEntryTrigger("PathName", gg_rct_REGIONNAME)

Lua:
-- Region Path System
RegionPathSystem = {
    paths = {}, -- Table to store paths by ID (divided into segments)
    pathPlayers = {}, -- Table to associate paths with specific players
    unitSegmentIndex = {}, -- Tracks the current segment index for units
}

-- Function to add a region to a path
-- @param pathID: The ID of the path to add the region to
-- @param region: The region to add to the path
function RegionPathSystem:AddRegionToPath(pathID, region)
    if not self.paths[pathID] then
        self.paths[pathID] = {} -- Create a new path if it doesn't exist
    end

    -- Add the region to the last segment, or create a new segment if necessary
    local segments = self.paths[pathID]
    local lastSegment = segments[#segments]

    if not lastSegment or #lastSegment >= 12 then
        lastSegment = {}
        table.insert(segments, lastSegment)
    end

    table.insert(lastSegment, region)

    -- Automatically set up a trigger for the last region in the segment
    if #lastSegment == 12 then
        SetupRegionEntryTrigger(pathID, region)
    end
end

-- Function to associate a path with a specific player
-- @param pathID: The ID of the path
-- @param player: The player to associate with the path
function RegionPathSystem:SetPathPlayer(pathID, player)
    self.pathPlayers[pathID] = player
end

-- Function to get the current segment for a unit
-- @param pathID: The ID of the path the unit is following
-- @param unit: The unit whose current segment is being checked
-- @return The current segment and its index
function RegionPathSystem:GetCurrentSegment(pathID, unit)
    local segmentIndex = self.unitSegmentIndex[unit] or 1
    local segments = self.paths[pathID]
    return segments and segments[segmentIndex], segmentIndex
end

-- Function to move a unit to the next segment
-- @param pathID: The ID of the path the unit is following
-- @param unit: The unit to advance to the next segment
-- @return The next segment, or nil if all segments are completed
function RegionPathSystem:AdvanceToNextSegment(pathID, unit)
    local segments = self.paths[pathID]
    if not segments then
        --print("Path with ID '" .. pathID .. "' does not exist.")
        return
    end

    local currentIndex = self.unitSegmentIndex[unit] or 1
    if currentIndex < #segments then
        self.unitSegmentIndex[unit] = currentIndex + 1
        return segments[self.unitSegmentIndex[unit]]
    else
        --print("Unit has completed all segments of path '" .. pathID .. "'")
        return nil
    end
end

-- Function to handle units entering a region and transitioning to the next segment
-- @param pathID: The ID of the path the unit is following
-- @param enteringUnit: The unit entering the region
function RegionPathSystem:HandleUnitEnteringPath(pathID, enteringUnit)
    local segment, segmentIndex = self:GetCurrentSegment(pathID, enteringUnit)
    if not segment then
        --print("Path with ID '" .. pathID .. "' does not exist or has no segments.")
        return
    end

    local owner = GetOwningPlayer(enteringUnit)
    local allowedPlayer = self.pathPlayers[pathID]
    if owner ~= allowedPlayer then
        --print("Unit's owner is not allowed to use path '" .. pathID .. "'.")
        return
    end

    -- Move the unit through each region in the segment
    for i, region in ipairs(segment) do
        local rectCenterX, rectCenterY = GetRectCenterX(region), GetRectCenterY(region)
        BlzQueuePointOrderById(enteringUnit, 851983, rectCenterX, rectCenterY) -- Move to region Thank you Waterknight for ID
    end

    -- Automatically move to the next segment after completing this one
    local nextSegment = self:AdvanceToNextSegment(pathID, enteringUnit)
    if nextSegment then
        local firstRegion = nextSegment[1]
        if firstRegion then
            SetupRegionEntryTrigger(pathID, firstRegion)
        end
    else
        --print("Unit has completed all segments for path '" .. pathID .. "'")
    end
end

-- Trigger setup for region entry
RegionTriggers = RegionTriggers or {}

-- Utility function to check if an object is a region
-- @param object: The object to check
-- @return True if the object is a region, false otherwise
function IsRegion(object)
    return object and type(object) == "userdata" and tostring(object):find("region")
end

-- Utility function to convert a rect to a region
-- @param rect: The rect to convert
-- @return The created region
function ConvertRectToRegion(rect)
    if not rect then
        --print("Error: Attempted to convert a nil rect to a region.")
        return nil
    end

    local region = CreateRegion()
    RegionAddRect(region, rect)
    return region
end

-- Trigger setup for region entry
-- @param pathID: The ID of the path
-- @param firstRegion: The first region in the segment to set up the trigger for
function SetupRegionEntryTrigger(pathID, firstRegion)

    -- Validate firstRegion
    if not firstRegion then
        --print("Error: firstRegion is nil for path '" .. pathID .. "'. Aborting setup.")
        return
    end

    if not IsRegion(firstRegion) then
        firstRegion = ConvertRectToRegion(firstRegion)
        if not firstRegion then
            --print("Error: Failed to convert firstRegion to a valid region.")
            return
        end
    end

    if not RegionTriggers[firstRegion] then
        local newTrigger = CreateTrigger()
        if not newTrigger then
            return
        end

        TriggerRegisterEnterRegion(newTrigger, firstRegion, nil)
        RegionTriggers[firstRegion] = newTrigger
    else
        --print("Using existing trigger for region: " .. tostring(firstRegion))
    end

    local regionTrigger = RegionTriggers[firstRegion]
    TriggerAddAction(regionTrigger, function()
        local enteringUnit = GetEnteringUnit()
        if enteringUnit then
            RegionPathSystem:HandleUnitEnteringPath(pathID, enteringUnit)
        else
            --print("Warning: No unit entered the region.")
        end
    end)
end

-- Example Initialization Setup
function InitSetup()

    RegionPathSystem:AddRegionToPath("PathName", gg_rct_REGIONNAME)

    RegionPathSystem:SetPathPlayer("PathName", Player(0))
    SetupRegionEntryTrigger("PathName", gg_rct_REGIONNAME)

end
[LIST=1]
[*]
[/LIST]

How to install​

Simply copy and paste the folder from the test map into your map, and follow the examples to set up your own pathing.

ChangeLog​

v1.0 [Initial Release]
Contents

Midnighters Pathing System (Map)

Reviews
Antares
No updates were made. Awaiting Update
Level 9
Joined
Oct 20, 2010
Messages
228
Love the system! I want to make use of it but there are some features I've ended up hacking in myself. I am hoping you could integrate the following features: the ability to remove a region from a path or even replace it with a different region. And lastly, a function to add a single unit to a given path.
 
I can see what I can do with that!
Love the system! I want to make use of it but there are some features I've ended up hacking in myself. I am hoping you could integrate the following features: the ability to remove a region from a path or even replace it with a different region. And lastly, a function to add a single unit to a given path.
 
I'm not a big fan of the forced colon syntax. This makes it look like RegionPathSystem is an instantiable object and might be confusing to users who are new to Lua. Meanwhile, there is really little benefit to doing it. You could search and replace self with RegionPathSystem or assign it to self in the first line of each function and the user wouldn't have to worry about it.

Your emmy annotations are not correctly triggering the type checks. They should look like this:
Lua:
---@param enteringUnit unit: The unit entering the region
It is especially not clear which types pathID is allowed to be.

unitSegmentIndex should have __mode = "k" so that it doesn't prevent garbage collection.

The InitSetup function should be its own script as it's part of the examples/test map.

The ability to pass a region array into AddRegionToPath could be nice to reduce boilerplate.
 
I'm not a big fan of the forced colon syntax. This makes it look like RegionPathSystem is an instantiable object and might be confusing to users who are new to Lua. Meanwhile, there is really little benefit to doing it. You could search and replace self with RegionPathSystem or assign it to self in the first line of each function and the user wouldn't have to worry about it.
I guess I can understand that, however, the map this system was designed for ties the system into numerous other systems within the map, made it a bit easier in my mind, I could see how it'd be confusing though.

The InitSetup function should be its own script as it's part of the examples/test map.

The ability to pass a region array into AddRegionToPath could be nice to reduce boilerplate.
I will change that in an update I will make, I completely forgot it was at the bottom of the script, if I'm being honest.

I can also add in a function to allow region arrays rather than just singular regions. I designed it the way I did incase someone decided they wanted to use triggers/lua commands to setup paths in game, rather than on startup. Something like a TD builder that has a player that chooses where the pathing is, rather than preset.

As for the pathID clarity, it can be anything, a string, number, phrase, whatever. Didn't think I really needed to clarify that, butt I will in the update.

Love the system! I want to make use of it but there are some features I've ended up hacking in myself. I am hoping you could integrate the following features: the ability to remove a region from a path or even replace it with a different region. And lastly, a function to add a single unit to a given path.
I have added the add and remove path points in my current version, I have a few other ideas I might mess around with. Still currently figuring out what you mean by adding a unit to the path, the way I use it in the map I am working on is spawn the unit through a seperate function/trigger, and then simply order it to move to the first region. You could alternatively spawn it within the first region.

I'm considering allowing an optional definition of time between arriving and leaving the region as well, incase someone might want to use this as a pathing system for NPCs in an oRPG/RPG.
 
Last edited:
Top