kuligs2 With separate apps, this would not be a problem. As I said, the problem is only due to how you structured this dummy self-pinging code.

You can structure the code in multiple ways. The important thing for the self-pinging app is to ensure that all 3 parts are executed in the proper order inside the same frame:

  1. client pings server
  2. server pings back
  3. client receives the pingback.

Your original code was performing 1 and 2 in the current frame and 3 in the next frame. And that was because how you structured the chunks of your code to execute.

The takeaway message is to take care to understand how and when is each chunk of your code actually executed inside the single frame processing step.

kuligs2 What happens if you disable server's vsync?
You can also try putting the server poll code into a high priority thread.

    xyz Server on laptop - vsync disabled

    Godot Engine v4.3.dev6.official.89850d553 - https://godotengine.org
    Vulkan 1.3.277 - Forward+ - Using Device #0: NVIDIA - NVIDIA GeForce RTX 3090
    
    ClientNode connected
    Timer started!
    latency: 66 ms
    latency: 88 ms
    latency: 6 ms
    latency: 6 ms
    latency: 6 ms
    latency: 6 ms
    latency: 5 ms
    latency: 4 ms
    latency: 5 ms
    latency: 5 ms
    latency: 5 ms
    latency: 4 ms
    latency: 48 ms
    latency: 73 ms
    latency: 94 ms
    latency: 14 ms
    latency: 38 ms
    latency: 5 ms
    latency: 87 ms
    latency: 8 ms
    latency: 6 ms
    latency: 55 ms
    latency: 77 ms
    latency: 103 ms
    latency: 26 ms
    latency: 50 ms
    latency: 6 ms
    latency: 94 ms
    latency: 5 ms
    latency: 42 ms
    latency: 5 ms
    latency: 92 ms
    latency: 107 ms
    latency: 36 ms
    latency: 6 ms
    latency: 67 ms
    latency: 67 ms
    latency: 5 ms
    --- Debugging process stopped ---

    vsync enabled

    Godot Engine v4.3.dev6.official.89850d553 - https://godotengine.org
    Vulkan 1.3.277 - Forward+ - Using Device #0: NVIDIA - NVIDIA GeForce RTX 3090
    
    ClientNode connected
    Timer started!
    latency: 28 ms
    latency: 58 ms
    latency: 78 ms
    latency: 111 ms
    latency: 24 ms
    latency: 42 ms
    latency: 72 ms
    latency: 88 ms
    latency: 122 ms
    latency: 38 ms
    latency: 69 ms
    latency: 103 ms
    latency: 114 ms
    latency: 39 ms
    latency: 53 ms
    latency: 86 ms
    latency: 103 ms
    latency: 36 ms
    latency: 53 ms
    latency: 13 ms
    latency: 97 ms
    latency: 114 ms
    latency: 47 ms
    latency: 63 ms
    latency: 100 ms
    latency: 117 ms
    latency: 52 ms
    latency: 66 ms
    latency: 100 ms
    latency: 111 ms
    latency: 28 ms
    latency: 61 ms
    latency: 78 ms
    latency: 109 ms
    latency: 25 ms
    latency: 59 ms
    latency: 72 ms
    latency: 89 ms
    latency: 25 ms
    latency: 41 ms
    --- Debugging process stopped ---
    • xyz replied to this.

      kuligs2 Try testing with both vsyncs disabled, and also try swapping the machines.

        xyz both vsync disabled

        Godot Engine v4.3.dev6.official.89850d553 - https://godotengine.org
        Vulkan 1.3.277 - Forward+ - Using Device #0: NVIDIA - NVIDIA GeForce RTX 3090
        
        ClientNode connected
        Timer started!
        latency: 171 ms
        latency: 192 ms
        latency: 103 ms
        latency: 26 ms
        latency: 51 ms
        latency: 74 ms
        latency: 98 ms
        latency: 19 ms
        latency: 47 ms
        latency: 69 ms
        latency: 91 ms
        latency: 115 ms
        latency: 36 ms
        latency: 60 ms
        latency: 84 ms
        latency: 108 ms
        latency: 29 ms
        latency: 53 ms
        latency: 78 ms
        latency: 102 ms
        latency: 23 ms
        latency: 47 ms
        latency: 71 ms
        latency: 95 ms
        latency: 16 ms
        --- Debugging process stopped ---

        Swapped machines:

        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: 8 ms
        latency: 8 ms
        latency: 10 ms
        latency: 8 ms
        latency: 5 ms
        latency: 9 ms
        latency: 7 ms
        latency: 8 ms
        latency: 9 ms
        latency: 8 ms
        latency: 14 ms
        latency: 14 ms
        latency: 14 ms
        latency: 14 ms
        latency: 14 ms
        latency: 14 ms
        latency: 7 ms
        latency: 14 ms
        latency: 14 ms
        latency: 14 ms
        latency: 7 ms
        latency: 7 ms
        latency: 7 ms
        --- Debugging process stopped ---
        • xyz replied to this.

          kuligs2 Now I'm curious to see how an endless loop in a thread would perform 🙂

            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 😉