Jump to content

Recommended Posts

Earlier this week I posted a question regarding pathfinding so I could implement the ability to right-click on the map screen to initiate travel, unfortunately it seemed that the game's native pathfinding system doesn't support long-distance travel.

 

So I present to you modders, Trailblazer!

Trailblazer is a (beta) pathfinding API that is capable of finding a path through entire maps!

Currently it finds paths across huge worlds (without looping or branching, intended to be the longest paths possible) in about a second (usually a little less).

 

If it interests you, feel free to grab my Map Travel mod as an example on how to use this.

If you find any bugs, please report them here.

And if you find anything I did wrong, please tell me so I can learn from it!

Finally, if you're an aspiring artist, feel free to make me an icon, and I'll give you credit in the mod's description!

Link to comment
https://forums.kleientertainment.com/forums/topic/72219-mod-beta-trailblazer/
Share on other sites

Is pathfinding already better than the ingame pathfinding which the waypoint mod uses?

And are there conflicts with minimap mod?
I think it would be the best, to disable the "rightclick-travel" for minimap... but if possible you could add option to allow it to work even for minimap.

And are there lags when trying to find the path?
When I remember right, the waypoint mod added the option to disable the ingame pathfinding thing, cause it causes lag on cluttered servers.

Edited by Serpens
1 hour ago, Serpens said:

Is pathfinding already better than the ingame pathfinding which the waypoint mod uses?

The ingame pathfinding algorithm just returns a straight line if the path exceeds a given length (probably 40 units), but this algorithm will traverse the entire map, so in this sense it's more robust. However it's currently single-threaded for simplicity of development, if that becomes an issue I'm sure I can make it multithreaded or concurrent (given that LUA supports either)

 

1 hour ago, Serpens said:

And are there conflicts with minimap mod?
I think it would be the best, to disable the "rightclick-travel" for minimap... but if possible you could add option to allow it to work even for minimap.

I don't think there should be conflicts with the minimap mod, but I'll have to check.

The right-click to travel portion is actually a separate mod, this only implements a pathfinding algorithm, and a simple way to invoke movement using it.

 

1 hour ago, Serpens said:

... are there lags when trying to find the path?
When I remember right, the waypoint mod added the option to disable the ingame pathfinding thing, cause it causes lag on cluttered servers.

I'm not sure as of yet how it will affect the networking side of things, but I've got plans to test it tonight with a friend (testing both client and host side), but that's the most testing I can give it as I don't have a server with quite a few people. So I might need some help from you guys in this respect.

However, if there are lag issues, I'm sure LUA has a multithreading (or at least a concurrency) API that would solve this problem.

 

- Codesmith

Edited by Codesmith512
fixed typo

And it is not a good idea to replace the locomotor component.
Better add this to your modmain:
 

Spoiler

local ARRIVE_STEP = .15 -- I think we can't access the local one in origial locomotor ...
AddComponentPostInit("locomotor", function(self)
	local _PreviewAction = self.PreviewAction
	self.PreviewAction = function(self, bufferedaction, run, try_instant)
        if bufferedaction == nil then
            return
        end
        if bufferedaction.action == GLOBAL.ACTIONS.TRAILBLAZE then
            self.throttle = 1
            self:Clear()
            self:Trailblaze(bufferedaction.pos, bufferedaction, run)
        else
            return _PreviewAction(self, bufferedaction, run, try_instant) 
        end
	end
    
    local _PushAction = self.PushAction
	self.PushAction = function(self, bufferedaction, run, try_instant)
        if bufferedaction == nil then
            return
        end
        if bufferedaction.action == GLOBAL.ACTIONS.TRAILBLAZE then
            self.throttle = 1
            local success, reason = bufferedaction:TestForStart()
            if not success then
                self.inst:PushEvent("actionfailed", { action = bufferedaction, reason = reason })
                return
            end
            self:Clear()
            self:Trailblaze(bufferedaction.pos, bufferedaction, run)
            if self.inst.components.playercontroller ~= nil then
                self.inst.components.playercontroller:OnRemoteBufferedAction()
            end
        else
            return _PushAction(self, bufferedaction, run, try_instant) 
        end
	end
    
    self.Trailblaze = function(self, pt, bufferedaction, run, overridedest)
        print("TRAILBLAZE!!!!")
        
        self.dest = GLOBAL.Dest(overridedest, pt)
        --print("TRAILBLAZE!!!! "..GLOBAL.tostring(pt).." bufferedaction: "..GLOBAL.tostring(bufferedaction).." run: "..GLOBAL.tostring(run).." dest: "..GLOBAL.tostring(self.dest))
        self.throttle = 1

        self.arrive_dist =
            bufferedaction ~= nil
            and (bufferedaction.distance or GLOBAL.math.max(bufferedaction.action.mindistance or 0, ARRIVE_STEP))
            or ARRIVE_STEP
        
        self.wantstorun = run

        if self.directdrive then
            if run then
                self:RunForward()
            else
                self:WalkForward()
            end
        else
            local p0 = GLOBAL.Vector3(self.inst.Transform:GetWorldPosition())
            local p1 = GLOBAL.Vector3(self.dest:GetPoint())
        
            local pathfinder = GLOBAL.require("components/trailblazer")
            local path = pathfinder.find_path(p0, p1, self.pathcaps)
                    
            if path.steps ~= nil then
                self.path = {}
                self.path.steps = path.steps
                self.path.currentstep = 2
                self.path.handle = nil
            end
        end
        self.wantstomoveforward = true
        self:SetBufferedAction(bufferedaction)
        self:StartUpdatingInternal()
    end
end)

-- Register trailblaze action (can be submitted to locomotor just like WALKTO; uses custom pathfinding algorithm)
AddAction("TRAILBLAZE", "Travel via Trailblaze", function(act) end)

 

 

Edited by Serpens
added spoiler and fixed code

thank you for response :)

Minimap mod makes no problems, but rightclick on it also won't do anything. I think this is okay, but if you have some time left, you could try to make it optionally work also with minimap mod :)

The conflict described below is fixed with the new code in modmain, without replacing the component files. Don't ask me why :D:)
The only conflict I found so far is the "auto-actions" mod, I already reported it at the mod at steamworkshop, cause I think this is something the author should fix and not you... but I'm not 100% sure.
http://steamcommunity.com/workshop/filedetails/discussion/651419070/365163686037074563/#c152390648088243595

Edited by Serpens

I fixed the code for locomoter above :)
The only thing we can not access is ARRIVE_STEP, so I added a local with same value at the beginning of modmain.
Now it works :)

About lagging:
Playing alone, the screen stops for a short time, when hitting in a long distance on the map.
The question is, if this also happens at clients pc, while the host (=server if without caves) rightclicks.

Edited by Serpens

I completely got my threads mixed up this weekend, so I missed your last comment.

I did all of my testing in a game with caves, so I'll need to do some more with just a standard multiplayer world, hopefully tonight.

Also, thanks for taking care of the locomotor code, I was curious to see how that was going to end up since it has a custom function.

Edited by Codesmith512

When I get a few minutes today I think I'm going to start implementing the concurrent routine, even if there isn't any networking issues (it would be weird if there weren't), it'll fix the entire game hanging during calculation, and it shouldn't be too difficult to implement (I hope...)

So LUA doesn't support multithreading (I should've guessed that a scripting language wouldn't), but I do have a concurrent method, I just need a way to update it once a frame.

I tried putting the call in 'Locomotor:OnUpdate', but that only updates while the player is walking (go figure...).

 

From the few basic tests I've done, it looks pretty smooth, I just need a reliable way to get it to be called each frame (or at a very regular interval)

Any Ideas?

 

Edit: I do see lots of mods using 'inst:DoPeriodicTask', but it would be ideal to be able to have a 'inst:RemovePeriodicTask'

Edited by Codesmith512
1 hour ago, Codesmith512 said:

I tried putting the call in 'Locomotor:OnUpdate', but that only updates while the player is walking (go figure...).

This isn't entirely accurate. OnUpdate is called when the game gets back around to calling it after calling all the other components with OnUpdate. I believe there is a specific amount of components which will update on any given frame hence the 'dt' variable of the OnUpdate function.

If you're wanting to use path finding to it's fullest, the optimal way would be to create a brain (these run on every frame [or just about]) and set it for the player when they right click on the map. If they decide to press any key, reset the brain back to the default player brain. This should give you the most optimal set-up.

21 minutes ago, Kzisor said:

This isn't entirely accurate. OnUpdate is called when the game gets back around to calling it after calling all the other components with OnUpdate. I believe there is a specific amount of components which will update on any given frame hence the 'dt' variable of the OnUpdate function.

This may also be the case, but the code I'm using looks like this

	-- Concurrently process trailblazer
	local _OnUpdate = self.OnUpdate
	self.OnUpdate = function(self, dt)
	
		if self.trailblazePath ~= nil then
			
			if trailblazer.processPath(self.trailblazePath, 1000) then
				
				if self.trailblazePath.nativePath.steps ~= nil then
					self.path = {}
					self.path.steps = self.trailblazePath.nativePath.steps
					self.path.currentstep = 2
					self.path.handle = nil
					
					self.wantstomoveforward = true
					self:SetBufferedAction(bufferedaction)
					self:StartUpdatingInternal()
				end
				
				self.trailblazer = nil
			end
			
			print("Processing Path...")
		end
		
		_OnUpdate(self, dt)
	end

If I click somewhere, and leave the computer for 1-2 minutes, nothing. I don't even get the print.
However, as soon as I click somewhere, and the player starts moving, I get "Processing Path..." in the console, and a fraction of a second later, the player starts going on my path instead.

 

That said, I don't need to update exactly every frame, but I do need some frequency (it takes ~27 calls to 'processPath' to finish an end-end map traversal).

I think I'm going to play with the 'DoPeriodicWork', coding a new brain and hotswapping them seems like overkill (even if it's not a medical procedure lol)

 

Edited by Codesmith512
14 minutes ago, Codesmith512 said:

This may also be the case, but the code I'm using looks like this

If I click somewhere, and leave the computer for 1-2 minutes, nothing. I don't even get the print.
However, as soon as I click somewhere, and the player starts moving, I get "Processing Path..." in the console, and a fraction of a second later, the player starts going on my path instead.

That said, I don't need to update exactly every frame, but I do need some frequency (it takes ~27 calls to 'processPath' to finish an end-end map traversal).

I think I'm going to play with the 'DoPeriodicWork', coding a new brain and hotswapping them seems like overkill (even if it's not a medical procedure lol)

 

That's strange and it probably is an error somewhere else in the code. You can test it by placing a print right underneath the function descriptor. However, when working with other components and the OnUpdate function; it always gets called periodically.

2 minutes ago, Codesmith512 said:

So the locomotor isn't an entity (I guess) so I can't use 'DoPeriodicTask'..

I'd rather not code a brain just for the update loop for a few cycles, but it may come to that.

Locomotor is a component, not an entity, the entity is what the locomotor is attached to.

3 hours ago, Kzisor said:

That's strange and it probably is an error somewhere else in the code. You can test it by placing a print right underneath the function descriptor. However, when working with other components and the OnUpdate function; it always gets called periodically.

Actually, the locomotor has a call to 'self:StopUpdatingInternal' in a couple of places that look like they intentionally disable the update loop if the entity is not traveling anywhere (they also have the inverse call to 'self:StartUpdatingInternal' when the locomotor is invoked to start traveling), so it seems intentional (to save resources?).

3 hours ago, Kzisor said:

Locomotor is a component, not an entity, the entity is what the locomotor is attached to.

Unfortunately, the DoPeriodicTask only seems to update 1 time per second tops...

 

I think my next attempt is going to be to tie into the 'Locomotor:OnUpdate'.. tomorrow..

Yes, I'm also sure that onUpdate for locomoter only updates when moving at least one tile. I use it for my "Homebase Bonus" mod, and in this function I do the speed change, if standing on special floor. If only standing there, nothing will happen. If moving small steps, nothing will hapen. Just if moving at least one (or two?) tile, the speed changes. And I'm also quite sure this behaviour is defined anywhere in this component, I think I also searched for this and found it, since I wondered about this behaviour.

For DoPeriodicTask there is a "Remove". You make it that way:

local mytask = inst:DoPeriodicTask(...)
if mytask~=nil then
    mytask:Cancel()
    mytask=nil
end

 

Edited by Serpens
7 hours ago, Codesmith512 said:

I think my next attempt is going to be to tie into the 'Locomotor:OnUpdate'.. tomorrow..

If that is the case, I haven't looked at the locomotor a great deal, then simply create a new component instead of using the locomotor. 

On 12/6/2016 at 5:09 AM, Serpens said:

For DoPeriodicTask there is a "Remove". You make it that way:


local mytask = inst:DoPeriodicTask(...)
if mytask~=nil then
    mytask:Cancel()
    mytask=nil
end

 

This and setting the time for the task to .05 or so is the key!

I've got a bug right now where sometimes the path just picks a random direction, but as soon as that's sorted out I'll push the concurrency update.

Edited by Codesmith512

I think I've fixed the bug, so I pushed the update!

Unless it breaks again, I'm hopefully going to fix the smoothing algorithm over the next few days; I'll push that, and if it looks good after a few days, I'll push it as the full release!

Thanks @Serpens and @Kzisor for the help!

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
  • Create New...