The custom functions seem straightforward enough but I haven't been able to find much on signals.

    • [deleted]

    • Best Answerset by Lethn

OH! Hang on, I may have just tweaked on how to do it, would I write a custom function within the signal and then have the thread start once the signal is activated?

Edit: hmmm no I'm declaring it wrong or starting it wrong, still not right.

You'll probably be more likely to get useful feedback on this if you shared some code too.

Morning.

Yes, what @Megalomaniak said.

If you use multithreading, be sure that operations are always in a well defined order and don't break each other's invariants. Like if you modify a scene in a thread make sure that other threads don't modify that scene as well. Even read-only access can go wrong if an operation has not completed yet to a consistent state. This is true for any data modifying operation.

That means for a programmer to synchronize access to data via mutexes. To signal between threads, for example if they can continue or to wait for an operation to conclude, you use semaphores.

That said, signals per se aren't thread safe, because they send notifications around to do things on occasions. In a multithreaded environment you'll have to synchronize these things. And there can be a lot of signals around. It is easier if all of them only perform read-operations on data, the scene tree, whatever, but if they start to modify things another thread is just working on then the outcome can be unexpected.

So the answer is yes, but depends on what's going on.

Edit: I wouldn't start a thread for a signal that fires frequently. Starting a thread on OS level is a costly operation, but idk if Godot threads are taken from a pool the engine has prepared, or really open an own thread like a low level language would. There's the buzz-word "thread pool", but idk if your case merits such efforts.

Honestly, if you think multi-threading will solve a problem, it will likely create more problems. It does have a potential to unlock performance, but only for things that are good to run parallel in specific situations. For the most part, games are sequential in nature, and have a fixed order of operations. For example, if you shoot a bullet, you expect the muzzle flash to happen first, then the bullet hits something, then there is some reaction (like blood particles, and explosion, etc.). Same with player logic, enemy logic, etc. This can't really be parallel, and even if you did it, the thread spawning and synchronization would make it slower than just writing it normally. Some things do make sense to run parallel. Like if you have a city and NPC car simulation, it would make sense to run this on a separate thread, particularly if there was not a lot of interaction with the player. Some games run input logic in another thread for lower latency. But even in AAA games, they usually spawn around 4 - 6 threads on launch and use this for specific stuff (as I said, an input thread, an AI thread, a rendering thread, etc.). Not spawning threads every time an event happens, this has no benefit. Though you could use thread pools. Like setup a pool an load time, which can take tasks you send it. This would avoid the thread creation slowdown, but you will still have to deal with synchronization, which can be extremely complex if you don't know what you are doing. Though it depends on the job and what you need.

Bear with me because that's a lot of information lol what I was thinking was when it came to the co-ordinates calculations and the calculations of the move_and_collide that I've got going with my navmesh agents I could use multi-threading to take off a lot of the load because that's where the strain on the main loop I believe is coming from. I'll post you guys code, but the problem is I know it's wrong since I'm getting errors, the setup isn't even really there yet.

var villagerRandomPositionThread = Thread.new()

func _ready():
	isWondering = true
	localVillageManager = get_parent_spatial()
	villagerRandomPositionThread.start(self, "villagerAgentRandomisedPosition")
func _on_MaleAdultNavigationAgent_navigation_finished():
	villagerRandomPositionThread.start(self, "villagerAgentRandomisedPosition")
	
func villagerAgentRandomisedPosition():
	randomGenerator.randomize()
	randomPoint = RandomPointInCircle(50.0)
	wonderVector3 = Vector3(localVillageManager.global_transform.origin.x + randomPoint.x, 0.0, localVillageManager.global_transform.origin.z + randomPoint.y)
	maleAdultNavigationAgent.set_target_location(wonderVector3)
	
func _exit_tree():
	villagerRandomPositionThread.wait_to_finish()

Found it all a bit baffling because as it turns out I managed to get the first randomised position code working on a thread without any errors in the _ready function, however signals are proving tricky. My main idea that I was going with was having a new thread start once a signal has been activated in the appropriate conditions, but I am very new at multi-threading.

villagerAgentRandomisedPosition() doesn't merit to run in an own thread. You may want to actually profile this, but I'd say creating a thread is slower than just running that method.

Uniformly distributed random point in circle has a sqrt(), a sin() and a cos(), but since the radius seems to be fixed you can spare the sqrt() and calculate it just once. sin and cos are fast on current processors. Faster than opening a thread. Plus there's a better cache usage with small, concise code, that is the most important thing to observe when it comes to execution speed.

Also, try calling randomize() only once, at the beginning of the program. You gain nothing from calling it every frame, only less reproducibility in the sequence of random numbers.

I believe these two changes and letting go of threads (for the time being and for this purpose only!) already should be visible when profiling.

Otoh, premature optimization and the the root of all evil and all that :-)

I can say nothing about synchronization from these snippets alone.

Well it's worth pointing out that I have 300 villagers so far wondering about using this code so I've been poking at it a lot I thought multi-threading might be a quick and dirty way to deal with my low FPS problem but it looks like I need to take a more considered approach.

Errr ... this may be a dumb question, but does that mean that there are 300 threads, one for each villager ?

    Pixophir LOL I don't know, I'm very new at this, but I guess yes? There's one script for each villager anyway, are threads supposed to be for entire nodes/used once?

    Instead of running a thread for every villager, the opposite might be better: Let one Manager node handle all villagers in the scene. You'll get the following advantages:

    • Manager can possibly skip calculations for large amount of villagers (because they are too far away from the player)
    • Manager might be able to reuse calculations for villagers in the same chunk/cell
    • Manager might be able to simplify calculation. If a villager is in a non-visible area and is supposed to "follow the player" you may want it to pass through walls etc. until it is closer to the player character. Players won't notice the difference.

    Also as a side note: Creating a single thread is very costly. That's what you do, if you have a script creating a single thread on each villager. It's kinda like ordering screws: Instead of ordering 1 screw, another screw, a third screw,... It's much faster to just order a box with like 20 screws or whatever.

    A manager can help with that too if really needed. Because the manager knows how many villagers there are. So instead of creating one thread at a time, it can create a bunch at once. Or it can run all the logic for all villagers in one single extra thread, that's always running in the background. Multithreading isn't easy to wrap your head around at the start. And honestly so far I only needed it on very rare occasions with Godot. I'd recommend to try looking into optimizations without mutlithreading first.

      MartinSenges Well it's handy that because I technically already have a manager node and that's to deal with things like spawning and hierarchy children among other things I just wasn' planning on using it the way you describe. This is the first time I've looked into optimising like this so it would be great if people could throw some resources and relevant documentation my way.

        Lethn To be honest I don't have any resources on that at hand currently, especially on Godot. This is stuff I picked up when I was still working with Game Frameworks like SDL etc. I'm pretty sure Handmade Hero covered this topic (or adjacent ones) at least partially in some video. But those videos are probably too hard to digest, due to their number, length and focus on C++.

        This is probably the best book on multi-threading. "C++ Concurrency in Action"

        It would be good to learn if you are serious about programming, but it may be a little advanced and not needed for a simple game.

        I second that.

        And that you should not have problems with C++17 and the STL.

        Been thinking and maybe I should look more at techniques like object pooling, but I'll have to poke around some more at the various optimisation techniques.

        Your use-case doesn't merit threading, it slows execution time down. It is also a case of premature optimisation, aka "the root of all evil".

        I think it is good advice to first finish the game and have all planned features realised. Then think about optimisation, of which concurrency may be a part. And never optimise just like so. Always profile the execution with timers and objectively identify the parts that use up most of the time.

        The tools Godot offers in GDscript are very limited anyway. They don't offer atomic (indivisible) operations for example, that may be better for this case, but I don"t know your project.

        I find the suggestion of a manager node a good start. There all the villagers are in one spot. Later on, if villager movement really is the crucial point as identified with profiling, you can still think of concurrency.

        Just my 2 cents ...

        But of course, if you have the time to play with concurrency, then why not :-)

        The problem is that with the 300 odd villagers I have in the game now it's chugging away at about 30fps so I don't think you can really make the argument of 'finish the game' in this particular case. I do think though that I need to look up more optimisation options beyond just multi-threading so I'll get back to researching on that. I plan on having something of a global villager count limit regardless that people can adjust for the sake of not killing lower end PCs, but if I can get a high number of villagers in the game and have it working smoothly I'm going to attempt it regardless.

          But does that game really need 300 villagers? Or at least on screen at once animating?

          Even AAA games like Cyberpunk 2077 or Assassin's Creed rarely have more than like 50 - 100 character one screen at once, and this is heavy for them.

            cybereality I'll need to have a think about it all I guess, it's very much a kind of city management style game at the end of the day, so there does need to be 'crowds' kind of like with settlers and stuff like that. I wonder how Tropico and City Skylines does their people rendering because that's something I immediately think of as a modern day example. If I can have it not rendering stuff off screen is that something possible for the sake of optimisation? Might be something to look at as well, lots of methods.

            For the record, the villagers play a big part in the gameplay, you can pick them up and throw them among other things.