xyz man, youre talking in riddles, or maybe youre just sarcastic, i cant tell.. Do you mean you want me to try to put the polling server thing in a separate thread or something? and same for the client? But how would that change anything?

  • xyz replied to this.

    kuligs2 But how would that change anything?

    The frequency of polling won't be tied to node processing i.e. it'd be fully independent of framerate. It'll run (almost) as fast as possible. Other alternative to try would be to just run the server code as a standalone script instead of the main game loop.

    kuligs2 man, youre talking in riddles, or maybe youre just sarcastic, i cant tell..

    No, I'm just assuming you're knowledgeable enough. If you're capable of writing an udp server you should be able to understand what "put it in a thread" means.

    Any code inside a _process() function acts as a part of the body of engine's main game loop. So basically the engine does this with your _process() function under the hood:

    while(running): #invisible
    	_process()
    	wait_for_next_frame() #invisible

    The problem is that the execution of the body of this "invisible loop" is delayed so it executes exactly once per frame. If you want it to run faster, you'll need the code in _process() to run in a loop that's decoupled from engine's main loop (runs in a thread) or to completely replace the main loop (runs as a custom standalone script instead of the default loop):

    func my_thread():
    	while(running):
    		# do stuff that was previously done in _process()

    The above loop will now iterate your code independently of engine's frame rate. Each loop iteration won't wait until the frame is finished. It will loop immediately. Assuming you launched that function as a thread or as a main game loop replacement.

      xyz You over estimated my abilities 😃. I just followed tutorial https://docs.godotengine.org/en/stable/classes/class_udpserver.html, via trial and error methods 😃. But thanks, now i have a vector to work with, Will try this later.

      This makes sense, but i thought that no matter where i execute function the game will process it once per frame or something, because its all in the same engine, like the process function is the most outter thread you can run your code on. So i didnt think there was posibility to create thread outside the main thread. I knew there was a thread function but i thought i was tied to the same engine loop.

      But i still dont get why i was getting 100ms when server was on low power machine, and client was on high end machine. And when swapped, i would get almost perfect latency times. Maybe because the high end pc could process more frames? I didnt measure FPS.

      Im no genius, i may be clever enough to deduct stuff from examples, but i dont know most things, because i dont have any backgrounds, no uni, nothing 😃. I do this out of curiosity, its no my day job or anything, just a hobby to pass time, and im learning many things now, how computers work etc. It turns out that there is a whole different world - computers.

      • xyz replied to this.

        kuligs2 I knew there was a thread function but i thought i was tied to the same engine loop.

        That would pretty much defeat the purpose of running a thread. Threads are useful in Godot precisely because framerate won't interfere with their execution. They'll run independently of the main loop and in parallel with it. So try running the server code in a thread and see what happens.

        Here's an example to compare the number of process calls vs the number of iterations in a thread loop that runs in parallel:

        extends Node
        
        var process_call_count = 0
        
        func _ready():
        	Thread.new().start(thread_func, Thread.PRIORITY_HIGH)
        
        func _process(delta):
        	print("process call count: ", process_call_count)
        	process_call_count += 1
        
        func thread_func():
        	var thread_loop_count = 0
        	while(true):
        		print("thread loop count: ", thread_loop_count)
        		thread_loop_count += 1

        Now I'm sure the difference between polling in _process() vs polling in a loop in a thread will become obvious.

          xyz Well, there you go again... mystery solved.

          ClientNode

          # client_node.gd
          class_name ClientNode
          extends Node
          
          var udp := PacketPeerUDP.new()
          
          var timer = Timer.new()
          var client_is_running = false
          var thread :Thread
          func _ready():
          	add_child(timer)
          	timer.wait_time=1
          	timer.timeout.connect(_on_timeout)
          	pass
          	
          func _process(_delta):
          	pass
          
          func _on_timeout():
          		#var ut = Time.get_unix_time_from_system()
          		var ms = str(Time.get_ticks_msec()) #str(int(ut * 1000))
          		
          		udp.put_packet(ms.to_utf8_buffer())
          		
          func start_ping(ip:String, port:int):
          	
          	var error = udp.connect_to_host(ip,port)
          
          	if error != OK:
          		print("ClientNode didnt connect")
          	else:
          		print("ClientNode connected")
          		timer.start()
          		print("Timer started!")
          		client_is_running = true
          		thread = Thread.new()
          		thread.start(thread_get_packet, Thread.PRIORITY_HIGH)
          	pass
          	
          func stop_ping():
          	
          	client_is_running = false
          	timer.stop()
          	udp.close()
          	thread.wait_to_finish()
          	print("ClientNode disconnected")
          	
          	pass
          
          func thread_get_packet():
          	
          	while(client_is_running):
          		if udp.get_available_packet_count() > 0:
          			#var ut = Time.get_unix_time_from_system()
          			var ms = Time.get_ticks_msec()
          			var packet_ms = udp.get_packet().get_string_from_utf8().to_int()
          			var latency = ms - packet_ms
          
          			print("latency: %s ms" % latency)

          ServerNode

          # server_node.gd
          class_name ServerNode
          extends Node
          
          var server := UDPServer.new()
          var peers = []
          var server_port = 4242
          var server_is_running = false
          var thread :Thread
          func _ready():
          	pass
          
          func _process(_delta):
          	pass
          
          func start_server(port):
          	var usable_port = server_port
          	
          	if port:
          		usable_port = port
          		
          	var error = server.listen(usable_port)
          	if error != OK:
          		print("ServerNode didnt listten")
          	else:
          		print("ServerNode listenning")
          		server_is_running = true
          		thread = Thread.new()
          		thread.start(thread_poll, Thread.PRIORITY_HIGH)
          	pass
          	
          func stop_server():
          	
          	
          	server_is_running= false
          	server.stop()
          	thread.wait_to_finish()
          	print("ServerNode stopped")	
          
          	pass
          	
          func thread_poll():
          	
          	while(server_is_running):
          		server.poll() # Important!
          		if server.is_connection_available():
          			var peer: PacketPeerUDP = server.take_connection()
          			var packet = peer.get_packet()
          
          			peer.put_packet(packet)
          			peers.append(peer)
          			
          		for i in range(0, peers.size()):
          			if peers[i].get_available_packet_count() > 0:
          				var packet = peers[i].get_packet()
          				peers[i].put_packet(packet)
          			pass # Do something with the connected peers.

          MainScene

          extends Control
          @onready var udp_ping_server: ServerNode = $UdpPingServer
          @onready var udp_ping_client: ClientNode = $UdpPingClient
          
          @onready var line_edit_ip: LineEdit = $PanelContainer/MarginContainer/VBoxContainer/MarginContainer/HBoxContainer/LineEditIP
          @onready var line_edit_port_client: LineEdit = $PanelContainer/MarginContainer/VBoxContainer/MarginContainer2/HBoxContainer/LineEditPortClient
          @onready var button_start_client: Button = $PanelContainer/MarginContainer/VBoxContainer/ButtonStartClient
          @onready var button_stop_client: Button = $PanelContainer/MarginContainer/VBoxContainer/ButtonStopClient
          @onready var line_edit_port_server: LineEdit = $PanelContainer/MarginContainer/VBoxContainer/MarginContainer3/HBoxContainer/LineEditPortServer
          @onready var button_start_server: Button = $PanelContainer/MarginContainer/VBoxContainer/ButtonStartServer
          @onready var button_stop_server: Button = $PanelContainer/MarginContainer/VBoxContainer/ButtonStopServer
          
          
          # Called when the node enters the scene tree for the first time.
          func _ready() -> void:
          	pass # Replace with function body.
          
          # Called every frame. 'delta' is the elapsed time since the previous frame.
          func _process(_delta: float) -> void:
          	pass
          
          func _on_button_start_client_button_down() -> void:
          	udp_ping_client.start_ping(line_edit_ip.text,line_edit_port_client.text.to_int())
          	pass # Replace with function body.
          
          func _on_button_stop_client_button_down() -> void:
          	udp_ping_client.stop_ping()
          	pass # Replace with function body.
          
          func _on_button_start_server_button_down() -> void:
          	udp_ping_server.start_server(line_edit_port_server.text.to_int())
          	pass # Replace with function body.
          
          func _on_button_stop_server_button_down() -> void:
          	udp_ping_server.stop_server()
          	pass # Replace with function body.

          So i merged everything in one app, and with main scene i orchestrate what happens. Manually start/stop client/server.

          Distributed this app across few local computers and now im getting better pings.

          Did a test with swapped machines, same result, just didnt record the results.

          So can we call it - problem solved? 😃 not sure how else to debug but to maybe host this over internet and try pinging machine somewhere in the world?

          EDIT: Fixed the thread thing, and updated the codes above

          upd-ping-43-threaded.zip
          6kB

            kuligs2 So can we call it - problem solved? 😃

            Mostly 🙂. You still need to do a proper thread cleanup to get rid of that warning. Store the thread object in some variable and call wait_to_finish() on it after you set the loop control variable to false
            So:

            var my_thread: Thread
            
            func start_server():
            	# all existing code except thread creation and start line 
            	# We'll rewrite it like following to keep a reference to the thread object:
            	my_thread = Thread.new()
            	my_thread.start(thread_poll, Thread.PRIORITY_HIGH)
            
            func stop_server():
            	# existing code
            	my_thread.wait_to_finish()

              xyz Thanks! Will do

              Also i did test over internet from office to home via wireguard vpn (locally hosted)

              Seems pretty legit. Im happy!

              Godot Engine v4.3.beta2.official.b75f0485b - https://godotengine.org
              Vulkan 1.3.277 - Forward+ - Using Device #0: NVIDIA - NVIDIA RTX A2000
              
              ClientNode connected
              Timer started!
              WARNING: A Thread object is being destroyed without its completion having been realized.
              Please call wait_to_finish() on it to ensure correct cleanup.
                   at: ~Thread (core/os/thread.cpp:105)
              latency: 3 ms
              latency: 15 ms
              latency: 2 ms
              latency: 9 ms
              latency: 15 ms
              latency: 11 ms
              latency: 11 ms
              latency: 2 ms
              latency: 4 ms
              latency: 2 ms
              ClientNode disconnected

              xyz Updated my post above with fixed code. Thanks for the help!!

              • xyz replied to this.

                kuligs2 Dude, not that I care too much but it's in poor taste to mark your own reply as the "best answer" just because you posted your final code there. I practically tutored you through the whole thread to that "answer".

                  xyz i dont care either, just wanted for newbies like myself in future to find solution. fixed

                  • xyz replied to this.

                    kuligs2 The solution is not in code snippets. It's in understanding things 😉

                      xyz Not so fast Hos!

                      Did atomic tests on laptop and high end pc..

                      Laptop to pc over wifi - lan:

                      Godot Engine v4.3.beta2.official.b75f0485b - https://godotengine.org
                      Vulkan 1.3.278 - Forward+ - Using Device #0: Intel - Intel(R) UHD Graphics 620 (WHL GT2)
                      
                      ClientNode connected
                      Timer started!
                      latency: 8 ms
                      latency: 7 ms
                      latency: 8 ms
                      latency: 6 ms
                      latency: 6 ms
                      latency: 6 ms
                      latency: 7 ms
                      latency: 7 ms
                      latency: 6 ms
                      latency: 7 ms
                      ClientNode disconnected
                      ServerNode listenning
                      --- Debugging process stopped ---

                      pc to laptop over wifi:

                      Vulkan 1.3.277 - Forward+ - Using Device #0: NVIDIA - NVIDIA GeForce RTX 3090
                      
                      ServerNode listenning
                      ClientNode connected
                      Timer started!
                      latency: 75 ms
                      latency: 3 ms
                      latency: 125 ms
                      latency: 3 ms
                      latency: 71 ms
                      latency: 94 ms
                      latency: 16 ms
                      latency: 40 ms
                      latency: 64 ms
                      latency: 94 ms
                      latency: 112 ms
                      latency: 33 ms
                      latency: 58 ms
                      ClientNode disconnected
                      --- Debugging process stopped ---

                      This makes no sense, if the timing was done the right way then the ping times should have atleast been similar on both devices over the same distance.

                      What im getting is that between computers with similar specs (the ones i tested yesterday) seems like it shows comparably true latency times, but when its high end vs low end then its not the case...

                      Also laptop is linux and pc is windows. Not sure if that changes anything. So im not sure what else to try.. 🙁

                      • xyz replied to this.

                        kuligs2 This makes no sense, if the timing was done the right way then the ping times should have atleast been similar on both devices over the same distance.

                        What "timing"?
                        If your client and server are polling as fast as possible, then the "problem" is with your networks and has nothing to do with Godot. It could also be the case that a thread is not getting enough of a time slice and/or not being put on a separate core. This again is the responsibility of the OS and Godot can do nothing about it.

                        You want the server to run as fast as possible. So it needs to be on a fast machine. You can try the other alternative I've suggested earlier, namely running the custom main loop and do everything in the main thread. Also don't forget to shut down any other unneeded work the server machine may be doing because it can steal cpu cycles that otherwise would be used by the server app.

                        In general, if you run the server and the client as separate apps on the same machine communicating via localhost - there should be zero latency. If that is indeed the case then there's nothing more that can be done on Godot side.

                          xyz If your client and server are polling as fast as possible, then the "problem" is with your networks and has nothing to do with Godot

                          How come using "ping" command in terminal produces constant results and are not in sort of same margins that im getting on either of my godot application examples?

                          There is nothing wrong with the network as i can reproduce the "pings" ICMP, from one to other computer with close to similar results. in this case pc to laptop and vice versa 1-3ms... Not 100ms in one way and 8ms in other way.

                          xyz It could also be a case that a thread is not getting enough of a time slice and/or not being put on a separate core. This again is the responsibility of the OS and Godot can do nothing about it.

                          This could be the problem, as the problem occures only on low spec machine, while the high spec machine as server gives out good pings, and low spec server gives out delayed pings.

                          xyz You can try the other alternative I've suggested earlier, namely running the custom main loop and do everything in the main thread.

                          Are you talking about this? https://docs.godotengine.org/en/stable/classes/class_mainloop.html How is is different from running separate scripts? It seems like it is just a plain script that extends mainloop class? So in this case if i put the server code in this script, do i poll in the _process or create a thread like we did yesterday?

                          The whole idea behind this exercise is to eventually make a lobby system, where players host a game, game sends address to masterlist, other players query master list for hosted games, make connections to the hosted games, get game info, view all games in a browser and join. In other words - server browser.

                          I have figured out the masterlist thing, and game info thing, now i just wanted to show players the ping between their machine and the hosted game machine, so that they would be able to join the ones that are closer - lesser latency.

                          • xyz replied to this.

                            kuligs2
                            Then just execute ping via OS::execute()

                            Also note that ping typically uses plain IP protocol which is one layer below UDP socket protocol in the internet protocol suite. So it'll likely be faster. For the actual reference times try pinging with a tool that lets you specify the protocol.

                            Also, you can benchmark time between two consecutive iterations in the polling loop and see if the values corelates with the communication delay.

                              xyz

                              xyz Then just execute ping via OS::execute()

                              First of all didnt know you could do that. Secondly this would assume that these programs exist on the device, and in real world situations is not reliable. I could pack the libs along with godot but idk.. it could be last resort. Im sure there is a way to do it reliably in godot. I just need to figure it out.

                              xyz Also note that ping typically uses plain IP protocol which is one layer below UDP socket protocol in the internet protocol suite. So it'll likely be faster. For the actual reference times try pinging with a tool that lets you specify the protocol.

                              I know, but relatively speaking i would understand that i would get higher times using higher protocols like UDP but here problem is consistency. One device receives faster than the other while doing the same work... if both devices would show the same times, dont matter how high they are, then i could start thinking about other protocols, and in godot i dont thik there is anything lower than TCP or UDP..

                              xyz Also, you can benchmark time between two consecutive iterations in the polling loop and see if the values corelates with the communication delay.

                              What am i measuring here? For example now im measuring from put_packet until i get_packet. This is done on the client, server just get_packet and puts_packet back at the client. And this action is triggered by times once every second.

                              Measuring for fast polling happens?

                              poll
                              latency = current_time - last_poll_time?
                              last_poll_time = current_time

                              And then read out that latency variable? But the pollin would happen super fast as it runs like 100000/s or something..

                              • xyz replied to this.

                                kuligs2 One device receives faster than the other while doing the same work

                                You don't know if they do the same work. The work is not symmetrical so with machines of different power it's totally expectable that swapping it may give different benchmarks of their total work. Test the actual UDP protocol communication both ways with some reliable tool. Comparing UDP times with IP pings is comparing apples and oranges.

                                kuligs2 Measuring for fast polling happens?

                                Yeah, just get the average ballpark here and see if it's larger when there is larger reported latency. This will be a hint that the thing is caused by polling frequency (quite likely). Otherwise it's something else.

                                The whole point of doing all those tests is to determine if this it Godot's or OS's "fault", so you can properly decide where to intervene.